From e6fc8dec48f7b95120c115363ec84c72b319c26f Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Wed, 21 Aug 2019 22:12:15 -0400 Subject: [PATCH 01/64] potential appveyor fix --- gulpfile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 9263969ef..1476f7cd6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -214,7 +214,8 @@ gulp.task("karma", gulp.series("bundle-test", function (done) { done('Browser test Failures'); } else { console.log('Browser tests passed'); - done(); + + process.exit(); // for some reason, AppVeyor hangs, so doing this instead of done(); } }).start(); })); From 64cec27be9bbc07a2eeffa4c6b6015e25c360a16 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 14:00:37 -0400 Subject: [PATCH 02/64] fix for Microsoft/TSJS-lib-generator#559 --- gulpfile.js | 14 +++++++++++++- package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 9263969ef..41c551a11 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,7 @@ var gulp = require("gulp"), mocha = require("gulp-mocha"), istanbul = require("gulp-istanbul"), karma = require("karma"), + fs = require('fs'), del = require('del'); //****************************************************************************** @@ -102,6 +103,7 @@ gulp.task("build-es", function () { var tsDtsProject = tsc.createProject("tsconfig.json", { declaration: true, noResolve: false, + typescript: require("typescript") }); @@ -226,6 +228,16 @@ if (process.env.APPVEYOR) { gulp.task("test", gulp.series("mocha")); } +gulp.task('verify-dts', function(done){ + const file = fs.readFileSync(`${__dirname}/dts/syntax/constraint_helpers.d.ts`).toString(); + + if(file.includes('TimerHandler')){ + throw new Error('Microsoft/TSJS-lib-generator/issues/559 bug was re-introduced.'); + } + + done(); +}); + //****************************************************************************** //* DEFAULT //****************************************************************************** @@ -234,6 +246,6 @@ gulp.task("build", gulp.series("lint", gulp.parallel( "build-es", "build-lib", "build-amd", - "build-dts"), "build-test")); + "build-dts"), "verify-dts", "build-test")); gulp.task("default", gulp.series("clean", "build", "test")); diff --git a/package.json b/package.json index 2dc69e195..2cbed7036 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "run-sequence": "2.2.1", "sinon": "7.3.2", "tslint": "5.17.0", - "typescript": "^3.5.1", + "typescript": "3.0.3", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0" } From 44c314d4fc796b8960627ff7948246d4f8a7bdb2 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 18:50:04 -0400 Subject: [PATCH 03/64] full promise/async support, global onActivation/onDeactivation, binding onDeactivation --- .gitignore | 1 + src/annotation/pre_destroy.ts | 16 + src/bindings/binding.ts | 7 +- src/constants/error_msgs.ts | 6 + src/constants/metadata_keys.ts | 3 + src/container/container.ts | 211 ++++- src/container/container_snapshot.ts | 11 +- src/container/lookup.ts | 4 +- src/interfaces/interfaces.ts | 30 +- src/resolution/instantiation.ts | 42 +- src/resolution/resolver.ts | 79 +- src/syntax/binding_in_when_on_syntax.ts | 6 +- src/syntax/binding_on_syntax.ts | 7 +- src/syntax/binding_when_on_syntax.ts | 4 + test/container/container.test.ts | 81 ++ test/container/container_module.test.ts | 34 + test/resolution/resolver.test.ts | 1086 ++++++++++++++++++++++- 17 files changed, 1598 insertions(+), 30 deletions(-) create mode 100644 src/annotation/pre_destroy.ts diff --git a/.gitignore b/.gitignore index 2ff852ecf..dc1ac12a3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ src/**/*.js.map src/*.js.map type_definitions/**/*.js package-lock.json +.DS_store .idea diff --git a/src/annotation/pre_destroy.ts b/src/annotation/pre_destroy.ts new file mode 100644 index 000000000..9dd3d3841 --- /dev/null +++ b/src/annotation/pre_destroy.ts @@ -0,0 +1,16 @@ +import * as ERRORS_MSGS from "../constants/error_msgs"; +import * as METADATA_KEY from "../constants/metadata_keys"; +import { Metadata } from "../planning/metadata"; + +function preDestroy() { + return function (target: any, propertyKey: string) { + const metadata = new Metadata(METADATA_KEY.PRE_DESTROY, propertyKey); + + if (Reflect.hasOwnMetadata(METADATA_KEY.PRE_DESTROY, target.constructor)) { + throw new Error(ERRORS_MSGS.MULTIPLE_PRE_DESTROY_METHODS); + } + Reflect.defineMetadata(METADATA_KEY.PRE_DESTROY, metadata, target.constructor); + }; +} + +export { preDestroy }; diff --git a/src/bindings/binding.ts b/src/bindings/binding.ts index c5712e82b..c823c20a7 100644 --- a/src/bindings/binding.ts +++ b/src/bindings/binding.ts @@ -40,7 +40,10 @@ class Binding implements interfaces.Binding { public constraint: (request: interfaces.Request) => boolean; // On activation handler (invoked just before an instance is added to cache and injected) - public onActivation: ((context: interfaces.Context, injectable: T) => T) | null; + public onActivation: ((context: interfaces.Context, injectable: T) => T | Promise) | null; + + // On deactivation handler (invoked just before an instance is unbinded and removed from container) + public onDeactivation: ((injectable: T) => Promise | void) | null; public constructor(serviceIdentifier: interfaces.ServiceIdentifier, scope: interfaces.BindingScope) { this.id = id(); @@ -54,6 +57,7 @@ class Binding implements interfaces.Binding { this.factory = null; this.provider = null; this.onActivation = null; + this.onDeactivation = null; this.dynamicValue = null; } @@ -68,6 +72,7 @@ class Binding implements interfaces.Binding { clone.provider = this.provider; clone.constraint = this.constraint; clone.onActivation = this.onActivation; + clone.onDeactivation = this.onDeactivation; clone.cache = this.cache; return clone; } diff --git a/src/constants/error_msgs.ts b/src/constants/error_msgs.ts index 08ed3bd6d..9895c77ea 100644 --- a/src/constants/error_msgs.ts +++ b/src/constants/error_msgs.ts @@ -17,6 +17,8 @@ export const INVALID_BINDING_TYPE = "Invalid binding type:"; export const NO_MORE_SNAPSHOTS_AVAILABLE = "No snapshot available to restore."; export const INVALID_MIDDLEWARE_RETURN = "Invalid return type in middleware. Middleware must return!"; export const INVALID_FUNCTION_BINDING = "Value provided to function binding must be a function!"; +export const LAZY_IN_SYNC = (key: any) => `You are attempting to construct '${key}' in a synchronous way + but it has asynchronous dependencies.`; export const INVALID_TO_SELF_VALUE = "The toSelf function can only be applied when a constructor is " + "used as service identifier"; @@ -39,8 +41,12 @@ export const CONTAINER_OPTIONS_INVALID_AUTO_BIND_INJECTABLE = "Invalid Container export const CONTAINER_OPTIONS_INVALID_SKIP_BASE_CHECK = "Invalid Container option. Skip base check must " + "be a boolean"; +export const MULTIPLE_PRE_DESTROY_METHODS = "Cannot apply @preDestroy decorator multiple times in the same class"; export const MULTIPLE_POST_CONSTRUCT_METHODS = "Cannot apply @postConstruct decorator multiple times in the same class"; +export const ASYNC_UNBIND_REQUIRED = "Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"; export const POST_CONSTRUCT_ERROR = (...values: any[]) => `@postConstruct error in class ${values[0]}: ${values[1]}`; +export const PRE_DESTROY_ERROR = (...values: any[]) => `@preDestroy error in class ${values[0]}: ${values[1]}`; +export const ON_DEACTIVATION_ERROR = (...values: any[]) => `onDeactivation() error in class ${values[0]}: ${values[1]}`; export const CIRCULAR_DEPENDENCY_IN_FACTORY = (...values: any[]) => "It looks like there is a circular dependency " + `in one of the '${values[0]}' bindings. Please investigate bindings with` + diff --git a/src/constants/metadata_keys.ts b/src/constants/metadata_keys.ts index a0419536f..7677c5577 100644 --- a/src/constants/metadata_keys.ts +++ b/src/constants/metadata_keys.ts @@ -30,3 +30,6 @@ export const DESIGN_PARAM_TYPES = "design:paramtypes"; // used to identify postConstruct functions export const POST_CONSTRUCT = "post_construct"; + +// used to identify preDestroy functions +export const PRE_DESTROY = "pre_destroy"; diff --git a/src/container/container.ts b/src/container/container.ts index e420443aa..cef3b41f0 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -19,6 +19,8 @@ class Container implements interfaces.Container { public readonly options: interfaces.ContainerOptions; private _middleware: interfaces.Next | null; private _bindingDictionary: interfaces.Lookup>; + private _activations: interfaces.Lookup>; + private _deactivations: interfaces.Lookup>; private _snapshots: interfaces.ContainerSnapshot[]; private _metadataReader: interfaces.MetadataReader; @@ -91,6 +93,8 @@ class Container implements interfaces.Container { this._bindingDictionary = new Lookup>(); this._snapshots = []; this._middleware = null; + this._activations = new Lookup>(); + this._deactivations = new Lookup>(); this.parent = null; this._metadataReader = new MetadataReader(); } @@ -107,7 +111,9 @@ class Container implements interfaces.Container { containerModuleHelpers.bindFunction, containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, - containerModuleHelpers.rebindFunction + containerModuleHelpers.rebindFunction, + containerModuleHelpers.onActivationFunction, + containerModuleHelpers.onDeactivationFunction ); } @@ -126,7 +132,9 @@ class Container implements interfaces.Container { containerModuleHelpers.bindFunction, containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, - containerModuleHelpers.rebindFunction + containerModuleHelpers.rebindFunction, + containerModuleHelpers.onActivationFunction, + containerModuleHelpers.onDeactivationFunction ); } @@ -160,6 +168,34 @@ class Container implements interfaces.Container { // Removes a type binding from the registry by its key public unbind(serviceIdentifier: interfaces.ServiceIdentifier): void { + if (this._bindingDictionary.hasKey(serviceIdentifier)) { + const bindings = this._bindingDictionary.get(serviceIdentifier); + + for (const binding of bindings) { + const result = this.preDestroy(binding); + + if (result instanceof Promise) { + throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); + } + } + } + + try { + this._bindingDictionary.remove(serviceIdentifier); + } catch (e) { + throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); + } + } + + public async unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + if (this._bindingDictionary.hasKey(serviceIdentifier)) { + const bindings = this._bindingDictionary.get(serviceIdentifier); + + for (const binding of bindings) { + await this.preDestroy(binding); + } + } + try { this._bindingDictionary.remove(serviceIdentifier); } catch (e) { @@ -169,9 +205,45 @@ class Container implements interfaces.Container { // Removes all the type bindings from the registry public unbindAll(): void { + this._bindingDictionary.traverse((key, value) => { + for (const binding of value) { + const result = this.preDestroy(binding); + + if (result instanceof Promise) { + throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); + } + } + }); + this._bindingDictionary = new Lookup>(); } + public async unbindAllAsync(): Promise { + const promises: Promise[] = []; + + this._bindingDictionary.traverse((key, value) => { + for (const binding of value) { + const result = this.preDestroy(binding); + + if (result instanceof Promise) { + promises.push(result); + } + } + }); + + await Promise.all(promises); + + this._bindingDictionary = new Lookup>(); + } + + public onActivation(serviceIdentifier: interfaces.ServiceIdentifier, onActivation: interfaces.BindingActivation) { + this._activations.add(serviceIdentifier, onActivation); + } + + public onDeactivation(serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) { + this._deactivations.add(serviceIdentifier, onDeactivation); + } + // Allows to check if there are bindings available for serviceIdentifier public isBound(serviceIdentifier: interfaces.ServiceIdentifier): boolean { let bound = this._bindingDictionary.hasKey(serviceIdentifier); @@ -205,7 +277,12 @@ class Container implements interfaces.Container { } public snapshot(): void { - this._snapshots.push(ContainerSnapshot.of(this._bindingDictionary.clone(), this._middleware)); + this._snapshots.push(ContainerSnapshot.of( + this._bindingDictionary.clone(), + this._middleware, + this._activations.clone(), + this._deactivations.clone() + )); } public restore(): void { @@ -214,6 +291,8 @@ class Container implements interfaces.Container { throw new Error(ERROR_MSGS.NO_MORE_SNAPSHOTS_AVAILABLE); } this._bindingDictionary = snapshot.bindings; + this._activations = snapshot.activations; + this._deactivations = snapshot.deactivations; this._middleware = snapshot.middleware; } @@ -238,37 +317,145 @@ class Container implements interfaces.Container { // The runtime identifier must be associated with only one binding // use getAll when the runtime identifier is associated with multiple bindings public get(serviceIdentifier: interfaces.ServiceIdentifier): T { - return this._get(false, false, TargetTypeEnum.Variable, serviceIdentifier) as T; + return this._get(false, false, false, TargetTypeEnum.Variable, serviceIdentifier) as T; + } + + public getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + return this._get(true, false, false, TargetTypeEnum.Variable, serviceIdentifier) as Promise; } public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T { - return this._get(false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T; + return this._get(false, false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T; + } + + public getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise { + return this._get(true, false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as Promise; } public getNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T { return this.getTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } + public getNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise { + return this.getTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + } + // Resolves a dependency by its runtime identifier // The runtime identifier can be associated with one or multiple bindings public getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] { - return this._get(true, true, TargetTypeEnum.Variable, serviceIdentifier) as T[]; + return this._get(false, true, true, TargetTypeEnum.Variable, serviceIdentifier) as T[]; + } + + public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise[] { + return this._get(true, true, true, TargetTypeEnum.Variable, serviceIdentifier) as Promise[]; } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { - return this._get(false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T[]; + return this._get(false, false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T[]; + } + + public getAllTaggedAsync( + serviceIdentifier: interfaces.ServiceIdentifier, + key: string | number | symbol, + value: any + ): Promise[] { + return this._get(true, false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as Promise[]; } public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { return this.getAllTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } + public getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise[] { + return this.getAllTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); + } + public resolve(constructorFunction: interfaces.Newable) { const tempContainer = this.createChild(); tempContainer.bind(constructorFunction).toSelf(); return tempContainer.get(constructorFunction); } + private preDestroy(binding: Binding): Promise | void { + if (!binding.cache) { + return; + } + + if (binding.cache instanceof Promise) { + return binding.cache.then(async (resolved) => this.doDeactivation(binding, resolved)); + } + + return this.doDeactivation(binding, binding.cache); + } + + private doDeactivation( + binding: Binding, + instance: T, + iter?: IterableIterator<[number, interfaces.BindingDeactivation]> + ): void | Promise { + let constr: any; + + try { + constr = (instance as any).constructor; + } catch (ex) { + // if placing mocks in container (eg: TypeMoq), this could blow up as constructor is not stubbed + return; + } + + try { + if (this._deactivations.hasKey(binding.serviceIdentifier)) { + const deactivations = iter || this._deactivations.get(binding.serviceIdentifier).entries(); + + let deact = deactivations.next(); + + while (deact.value) { + const result = deact.value[1](instance); + + if (result instanceof Promise) { + return result.then(() => { + this.doDeactivation(binding, instance, deactivations); + }).catch((ex) => { + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); + }); + } + + deact = deactivations.next(); + } + } + } catch (ex) { + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); + } + + if (this.parent) { + return this.doDeactivation.bind(this.parent)(binding, instance); + } + + try { + if (typeof binding.onDeactivation === "function") { + const result = binding.onDeactivation(instance); + + if (result instanceof Promise) { + return result.then(() => this.destroyMetadata(constr, instance)); + } + } + + return this.destroyMetadata(constr, instance); + } catch (ex) { + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); + } + } + + private destroyMetadata(constr: any, instance: any) { + if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constr)) { + const data: interfaces.Metadata = Reflect.getMetadata(METADATA_KEY.PRE_DESTROY, constr); + try { + return instance[data.value](); + } catch (e) { + throw new Error(ERROR_MSGS.PRE_DESTROY_ERROR(constr.name, e.message)); + } + } + } + private _getContainerModuleHelpersFactory() { const setModuleId = (bindingToSyntax: any, moduleId: number) => { @@ -306,6 +493,8 @@ class Container implements interfaces.Container { return (mId: number) => ({ bindFunction: getBindFunction(mId), isboundFunction: getIsboundFunction(mId), + onActivationFunction: this.onActivation.bind(this), + onDeactivationFunction: this.onDeactivation.bind(this), rebindFunction: getRebindFunction(mId), unbindFunction: getUnbindFunction(mId) }); @@ -316,13 +505,14 @@ class Container implements interfaces.Container { // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve private _get( + async: boolean, avoidConstraints: boolean, isMultiInject: boolean, targetType: interfaces.TargetType, serviceIdentifier: interfaces.ServiceIdentifier, key?: string | number | symbol, value?: any - ): (T | T[]) { + ): (T | T[] | Promise | Promise[]) { let result: (T | T[]) | null = null; @@ -345,6 +535,10 @@ class Container implements interfaces.Container { result = this._planAndResolve()(defaultArgs); } + if (result instanceof Promise && !async) { + throw new Error(ERROR_MSGS.LAZY_IN_SYNC(serviceIdentifier)); + } + return result; } @@ -371,6 +565,7 @@ class Container implements interfaces.Container { // resolve plan const result = resolve(context); + return result; }; diff --git a/src/container/container_snapshot.ts b/src/container/container_snapshot.ts index b86639de1..53b3fcdb4 100644 --- a/src/container/container_snapshot.ts +++ b/src/container/container_snapshot.ts @@ -3,12 +3,21 @@ import { interfaces } from "../interfaces/interfaces"; class ContainerSnapshot implements interfaces.ContainerSnapshot { public bindings: interfaces.Lookup>; + public activations: interfaces.Lookup>; + public deactivations: interfaces.Lookup>; public middleware: interfaces.Next | null; - public static of(bindings: interfaces.Lookup>, middleware: interfaces.Next | null) { + public static of( + bindings: interfaces.Lookup>, + middleware: interfaces.Next | null, + activations: interfaces.Lookup>, + deactivations: interfaces.Lookup> + ) { const snapshot = new ContainerSnapshot(); snapshot.bindings = bindings; snapshot.middleware = middleware; + snapshot.deactivations = deactivations; + snapshot.activations = activations; return snapshot; } diff --git a/src/container/lookup.ts b/src/container/lookup.ts index 6194ce136..eada79287 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -1,7 +1,7 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import { interfaces } from "../interfaces/interfaces"; -class Lookup> implements interfaces.Lookup { +class Lookup | any> implements interfaces.Lookup { // dictionary used store multiple values for each key private _map: Map, T[]>; @@ -91,7 +91,7 @@ class Lookup> implements interfaces.Lookup { const copy = new Lookup(); this._map.forEach((value, key) => { - value.forEach((b) => copy.add(key, b.clone())); + value.forEach((b) => copy.add(key, b.clone ? b.clone() : b)); }); return copy; diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 3a03d3e17..d3500135c 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -42,6 +42,10 @@ namespace interfaces { clone(): T; } + export type BindingActivation = (context: interfaces.Context, injectable: T) => T | Promise; + + export type BindingDeactivation = (injectable: T) => void | Promise; + export interface Binding extends Clonable> { id: number; moduleId: string; @@ -54,7 +58,8 @@ namespace interfaces { implementationType: Newable | null; factory: FactoryCreator | null; provider: ProviderCreator | null; - onActivation: ((context: interfaces.Context, injectable: T) => T) | null; + onActivation: BindingActivation | null; + onDeactivation: BindingDeactivation | null; cache: T | null; } @@ -176,6 +181,14 @@ namespace interfaces { getAll(serviceIdentifier: ServiceIdentifier): T[]; getAllTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): T[]; getAllNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): T[]; + getAsync(serviceIdentifier: ServiceIdentifier): Promise; + getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise; + getTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): Promise; + getAllAsync(serviceIdentifier: ServiceIdentifier): Promise[]; + getAllTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): Promise[]; + getAllNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise[]; + onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation): void; + onDeactivation(serviceIdentifier: ServiceIdentifier, onDeactivation: BindingDeactivation): void; resolve(constructorFunction: interfaces.Newable): T; load(...modules: ContainerModule[]): void; loadAsync(...modules: AsyncContainerModule[]): Promise; @@ -209,18 +222,24 @@ namespace interfaces { bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, - rebind: interfaces.Rebind + rebind: interfaces.Rebind, + onActivation: interfaces.Container["onActivation"], + onDeactivation: interfaces.Container["onDeactivation"] ) => void; export type AsyncContainerModuleCallBack = ( bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, - rebind: interfaces.Rebind + rebind: interfaces.Rebind, + onActivation: interfaces.Container["onActivation"], + onDeactivation: interfaces.Container["onDeactivation"] ) => Promise; export interface ContainerSnapshot { bindings: Lookup>; + activations: Lookup>; + deactivations: Lookup>; middleware: Next | null; } @@ -236,7 +255,8 @@ namespace interfaces { } export interface BindingOnSyntax { - onActivation(fn: (context: Context, injectable: T) => T): BindingWhenSyntax; + onActivation(fn: (context: Context, injectable: T) => T | Promise): BindingWhenSyntax; + onDeactivation(fn: (injectable: T) => void | Promise): BindingWhenSyntax; } export interface BindingWhenSyntax { @@ -271,7 +291,7 @@ namespace interfaces { to(constructor: new (...args: any[]) => T): BindingInWhenOnSyntax; toSelf(): BindingInWhenOnSyntax; toConstantValue(value: T): BindingWhenOnSyntax; - toDynamicValue(func: (context: Context) => T): BindingInWhenOnSyntax; + toDynamicValue(func: (context: Context) => T | Promise): BindingInWhenOnSyntax; toConstructor(constructor: Newable): BindingWhenOnSyntax; toFactory(factory: FactoryCreator): BindingWhenOnSyntax; toFunction(func: T): BindingWhenOnSyntax; diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 1add363ac..540e617fa 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -1,4 +1,4 @@ -import { POST_CONSTRUCT_ERROR } from "../constants/error_msgs"; +import { ON_DEACTIVATION_ERROR, POST_CONSTRUCT_ERROR, PRE_DESTROY_ERROR } from "../constants/error_msgs"; import { TargetTypeEnum } from "../constants/literal_types"; import * as METADATA_KEY from "../constants/metadata_keys"; import { interfaces } from "../interfaces/interfaces"; @@ -9,7 +9,6 @@ function _injectProperties( childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler ): any { - const propertyInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => ( childRequest.target !== null && @@ -33,11 +32,11 @@ function _createInstance(Func: interfaces.Newable, injections: Object[]): a return new Func(...injections); } -function _postConstruct(constr: interfaces.Newable, result: any): void { +function _postConstruct(constr: interfaces.Newable, result: any): void | Promise { if (Reflect.hasMetadata(METADATA_KEY.POST_CONSTRUCT, constr)) { const data: Metadata = Reflect.getMetadata(METADATA_KEY.POST_CONSTRUCT, constr); try { - result[data.value](); + return result[data.value](); } catch (e) { throw new Error(POST_CONSTRUCT_ERROR(constr.name, e.message)); } @@ -45,27 +44,58 @@ function _postConstruct(constr: interfaces.Newable, result: any): void { } function resolveInstance( + binding: interfaces.Binding, constr: interfaces.Newable, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler ): any { + if (binding.scope === "Transient") { + if (typeof binding.onDeactivation === "function") { + throw new Error(ON_DEACTIVATION_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); + } + + if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constr)) { + throw new Error(PRE_DESTROY_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); + } + } let result: any = null; if (childRequests.length > 0) { - const constructorInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => (childRequest.target !== null && childRequest.target.type === TargetTypeEnum.ConstructorArgument)); const constructorInjections = constructorInjectionsRequests.map(resolveRequest); + if (constructorInjections.some((ci) => ci instanceof Promise)) { + return new Promise( async (resolve, reject) => { + try { + const resolved = await Promise.all(constructorInjections); + + result = _createInstance(constr, resolved); + result = _injectProperties(result, childRequests, resolveRequest); + + await _postConstruct(constr, result); + + resolve(result); + } catch (ex) { + reject(ex); + } + }); + } + result = _createInstance(constr, constructorInjections); result = _injectProperties(result, childRequests, resolveRequest); } else { result = new constr(); } - _postConstruct(constr, result); + + const post = _postConstruct(constr, result); + + if (post instanceof Promise) { + return post.then(() => result); + } return result; } diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index d0291e42b..29e1b79af 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -1,6 +1,7 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import { BindingScopeEnum, BindingTypeEnum } from "../constants/literal_types"; import { interfaces } from "../interfaces/interfaces"; +import { getBindingDictionary } from "../planning/planner"; import { isStackOverflowExeption } from "../utils/exceptions"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { resolveInstance } from "./instantiation"; @@ -98,6 +99,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => ); } else if (binding.type === BindingTypeEnum.Instance && binding.implementationType !== null) { result = resolveInstance( + binding, binding.implementationType, childRequests, _resolveRequest(requestScope) @@ -109,10 +111,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => throw new Error(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${serviceIdentifier}`); } - // use activation handler if available - if (typeof binding.onActivation === "function") { - result = binding.onActivation(request.parentContext, result); - } + result = onActivation(request, binding, result); // store in cache if scope is singleton if (isSingleton) { @@ -133,6 +132,78 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => }; +function onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T): T | Promise { + if (resolved instanceof Promise) { + return resolved.then((unpromised) => onActivation(request, binding, unpromised)); + } + + let result: T | Promise = resolved; + + // use activation handler if available + if (typeof binding.onActivation === "function") { + result = binding.onActivation(request.parentContext, result); + } + + const containers = [request.parentContext.container]; + + let parent = request.parentContext.container.parent; + + while (parent) { + containers.unshift(parent); + + parent = parent.parent; + } + + const iter = containers.entries(); + + return activationLoop(iter.next().value[1], iter, binding, request.serviceIdentifier, result); +} + +function activationLoop( + container: interfaces.Container, + containerIterator: IterableIterator<[number, interfaces.Container]>, + binding: interfaces.Binding, + identifier: interfaces.ServiceIdentifier, + previous: T | Promise, + iterator?: IterableIterator<[number, interfaces.BindingActivation]> + ): T | Promise { + if (previous instanceof Promise) { + return previous.then((unpromised) => activationLoop(container, containerIterator, binding, identifier, unpromised)); + } + + let result = previous; + + let iter = iterator; + + if (!iter) { + // smell accessing _activations, but similar pattern is done in planner.getBindingDictionary() + const activations = (container as any)._activations as interfaces.Lookup>; + + iter = activations.hasKey(identifier) ? activations.get(identifier).entries() : [].entries(); + } + + let next = iter.next(); + + while (!next.done) { + result = next.value[1](this, result); + + if (result instanceof Promise) { + return result.then((unpromised) => activationLoop(container, containerIterator, binding, identifier, unpromised, iter)); + } + + next = iter.next(); + } + + const nextContainer = containerIterator.next(); + + if (nextContainer.value && !getBindingDictionary(container).hasKey(identifier)) { + // make sure if we are currently on the container that owns the binding, not to keep looping down to child containers + return activationLoop(nextContainer.value[1], containerIterator, binding, identifier, result); + } + + return result; + } + function resolve(context: interfaces.Context): T { const _f = _resolveRequest(context.plan.rootRequest.requestScope); return _f(context.plan.rootRequest); diff --git a/src/syntax/binding_in_when_on_syntax.ts b/src/syntax/binding_in_when_on_syntax.ts index 5f381a669..b134c17f1 100644 --- a/src/syntax/binding_in_when_on_syntax.ts +++ b/src/syntax/binding_in_when_on_syntax.ts @@ -89,10 +89,14 @@ class BindingInWhenOnSyntax implements interfaces.BindingInSyntax, interfa return this._bindingWhenSyntax.whenNoAncestorMatches(constraint); } - public onActivation(handler: (context: interfaces.Context, injectable: T) => T): interfaces.BindingWhenSyntax { + public onActivation(handler: (context: interfaces.Context, injectable: T) => T | Promise): interfaces.BindingWhenSyntax { return this._bindingOnSyntax.onActivation(handler); } + public onDeactivation(handler: (injectable: T) => void | Promise): interfaces.BindingWhenSyntax { + return this._bindingOnSyntax.onDeactivation(handler); + } + } export { BindingInWhenOnSyntax }; diff --git a/src/syntax/binding_on_syntax.ts b/src/syntax/binding_on_syntax.ts index a885fc762..0489ab0e3 100644 --- a/src/syntax/binding_on_syntax.ts +++ b/src/syntax/binding_on_syntax.ts @@ -9,11 +9,16 @@ class BindingOnSyntax implements interfaces.BindingOnSyntax { this._binding = binding; } - public onActivation(handler: (context: interfaces.Context, injectable: T) => T): interfaces.BindingWhenSyntax { + public onActivation(handler: (context: interfaces.Context, injectable: T) => T | Promise): interfaces.BindingWhenSyntax { this._binding.onActivation = handler; return new BindingWhenSyntax(this._binding); } + public onDeactivation(handler: (injectable: T) => void | Promise): interfaces.BindingWhenSyntax { + this._binding.onDeactivation = handler; + return new BindingWhenSyntax(this._binding); + } + } export { BindingOnSyntax }; diff --git a/src/syntax/binding_when_on_syntax.ts b/src/syntax/binding_when_on_syntax.ts index 768154d10..83ab76fb3 100644 --- a/src/syntax/binding_when_on_syntax.ts +++ b/src/syntax/binding_when_on_syntax.ts @@ -78,6 +78,10 @@ class BindingWhenOnSyntax implements interfaces.BindingWhenSyntax, interfa return this._bindingOnSyntax.onActivation(handler); } + public onDeactivation(handler: (injectable: T) => Promise | void): interfaces.BindingWhenSyntax { + return this._bindingOnSyntax.onDeactivation(handler); + } + } export { BindingWhenOnSyntax }; diff --git a/test/container/container.test.ts b/test/container/container.test.ts index baeaef352..e0ba46133 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -804,4 +804,85 @@ describe("Container", () => { }); + it("Should be able to resolve named multi-injection (async)", async () => { + + interface Intl { + hello?: string; + goodbye?: string; + } + + const container = new Container(); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "bonjour" })).whenTargetNamed("fr"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "au revoir" })).whenTargetNamed("fr"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "hola" })).whenTargetNamed("es"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "adios" })).whenTargetNamed("es"); + + const fr = await Promise.all(container.getAllNamedAsync("Intl", "fr")); + expect(fr.length).to.equal(2); + expect(fr[0].hello).to.equal("bonjour"); + expect(fr[1].goodbye).to.equal("au revoir"); + + const es = await Promise.all(container.getAllNamedAsync("Intl", "es")); + expect(es.length).to.equal(2); + expect(es[0].hello).to.equal("hola"); + expect(es[1].goodbye).to.equal("adios"); + + }); + + it("Should be able to resolve named (async)", async () => { + interface Intl { + hello?: string; + goodbye?: string; + } + + const container = new Container(); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "bonjour" })).whenTargetNamed("fr"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "hola" })).whenTargetNamed("es"); + + const fr = await container.getNamedAsync("Intl", "fr"); + expect(fr.hello).to.equal("bonjour"); + + const es = await container.getNamedAsync("Intl", "es"); + expect(es.hello).to.equal("hola"); + }); + + it("Should be able to resolve tagged multi-injection (async)", async () => { + + interface Intl { + hello?: string; + goodbye?: string; + } + + const container = new Container(); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "bonjour" })).whenTargetTagged("lang", "fr"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "au revoir" })).whenTargetTagged("lang", "fr"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "hola" })).whenTargetTagged("lang", "es"); + container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "adios" })).whenTargetTagged("lang", "es"); + + const fr = await Promise.all(container.getAllTaggedAsync("Intl", "lang", "fr")); + expect(fr.length).to.equal(2); + expect(fr[0].hello).to.equal("bonjour"); + expect(fr[1].goodbye).to.equal("au revoir"); + + const es = await Promise.all(container.getAllTaggedAsync("Intl", "lang", "es")); + expect(es.length).to.equal(2); + expect(es[0].hello).to.equal("hola"); + expect(es[1].goodbye).to.equal("adios"); + + }); + + it("Should be able to get a tagged binding (async)", async () => { + + const zero = "Zero"; + const isValidDivisor = "IsValidDivisor"; + const container = new Container(); + + container.bind(zero).toDynamicValue(() => Promise.resolve(0)).whenTargetTagged(isValidDivisor, false); + expect(await container.getTaggedAsync(zero, isValidDivisor, false)).to.equal(0); + + container.bind(zero).toDynamicValue(() => Promise.resolve(1)).whenTargetTagged(isValidDivisor, true); + expect(await container.getTaggedAsync(zero, isValidDivisor, false)).to.equal(0); + expect(await container.getTaggedAsync(zero, isValidDivisor, true)).to.equal(1); + + }); }); diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index 12d2cf0c1..1e07bdd51 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -118,4 +118,38 @@ describe("ContainerModule", () => { }); + it("Should be able to add an activation hook through a container module", () => { + + const container = new Container(); + container.bind("A").toConstantValue("1"); + expect(container.get("A")).to.eql("1"); + + const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation) => { + onActivation("A", () => "B"); + }); + + container.load(warriors); + + expect(container.get("A")).to.eql("B"); + + }); + + it("Should be able to add an deactivation hook through a container module", () => { + const container = new Container(); + container.bind("A").toConstantValue("1"); + + let deact = false; + + const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + onDeactivation("A", () => { + deact = true; + }); + }); + + container.load(warriors); + container.get("A"); + container.unbind("A"); + + expect(deact).eql(true); + }); }); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 3f53e837f..8cbeb4272 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -6,6 +6,7 @@ import { injectable } from "../../src/annotation/injectable"; import { multiInject } from "../../src/annotation/multi_inject"; import { named } from "../../src/annotation/named"; import { postConstruct } from "../../src/annotation/post_construct"; +import { preDestroy } from "../../src/annotation/pre_destroy"; import { tagged } from "../../src/annotation/tagged"; import { targetName } from "../../src/annotation/target_name"; import * as ERROR_MSGS from "../../src/constants/error_msgs"; @@ -1074,7 +1075,7 @@ describe("Resolve", () => { } } - expect(resolveInstance.bind(resolveInstance, Katana, [], (request: interfaces.Request) => null)) + expect(() => resolveInstance({} as interfaces.Binding, Katana, [], () => null)) .to.throw("@postConstruct error in class Katana: Original Message"); }); @@ -1169,4 +1170,1087 @@ describe("Resolve", () => { }); + it("Should support async when default scope is singleton", async () => { + const container = new Container({defaultScope: "Singleton"}); + container.bind("a").toDynamicValue( async () => Math.random()); + + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); + + expect(object1).equals(object2); + }); + + it("Should return different values if default singleton scope is overriden by bind", async () => { + const container = new Container({defaultScope: "Singleton"}); + container.bind("a").toDynamicValue( async () => Math.random()).inTransientScope(); + + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); + + expect(object1).not.equals(object2); + }); + + it("Should only call parent async singleton once within child containers", async () => { + const parent = new Container(); + parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const [subject1, subject2] = await Promise.all([ + parent.getAsync("Parent"), + parent.getAsync("Parent") + ]); + + expect(subject1 === subject2).eql(true); + }); + + it("Should return resolved instance to onDeactivation when binding is async", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() + .onDeactivation((instance) => new Promise((r) => { + expect(instance).instanceof(Destroyable); + r(); + })); + + await container.getAsync("Destroyable"); + + await container.unbindAsync("Destroyable"); + }); + + it("Should wait on deactivation promise before returning unbindAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); + + resolved = true; + })); + + container.get("Destroyable"); + + await container.unbindAsync("Destroyable"); + + expect(resolved).eql(true); + }); + + it("Should wait on predestroy promise before returning unbindAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r(); + + resolved = true; + }); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + await container.unbindAsync("Destroyable"); + + expect(resolved).eql(true); + }); + + it("Should wait on deactivation promise before returning unbindAllAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); + + resolved = true; + })); + + container.get("Destroyable"); + + await container.unbindAllAsync(); + + expect(resolved).eql(true); + }); + + it("Should wait on predestroy promise before returning unbindAllAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r(); + + resolved = true; + }); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + await container.unbindAllAsync(); + + expect(resolved).eql(true); + }); + + it("Should not allow transient construction with async preDestroy", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope(); + + expect(() => container.get("Destroyable")).to + .throw("@preDestroy error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should not allow transient construction with async deactivation", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope() + .onDeactivation(() => Promise.resolve()); + + expect(() => container.get("Destroyable")).to + .throw("onDeactivation() error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should force a class with an async deactivation to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async pre destroy to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async deactivation to use the async unbind api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should invoke destory in order (all async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve(); + }); + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + parent = roll; + roll += 1; + presolve(); + }); + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { + binding = roll; + roll += 1; + presolve(); + })); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); + + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve(); + }); + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); + + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + klass = roll; + roll += 1; + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + child = roll; + roll += 1; + }); + + childContainer.get("Destroyable"); + childContainer.unbind("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should force a class with an async pre destroy to use the async unbind api", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async onActivation to use the async api", async () => { + @injectable() + class Constructable { + } + + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation(() => Promise.resolve()); + + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + but it has asynchronous dependencies.`); + }); + + it("Should force a class with an async post construct to use the async api", async () => { + @injectable() + class Constructable { + @postConstruct() + public myPostConstructMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Constructable").to(Constructable); + + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + but it has asynchronous dependencies.`); + }); + + it("Should return resolved instance to onActivation when binding is async", async () => { + @injectable() + class Constructable { + } + + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + expect(c).instanceof(Constructable); + + r(c); + })); + + await container.getAsync("Constructable"); + }); + + it("Should not allow sync get if an async activation was added to container", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); + + container.onActivation("foo", () => Promise.resolve("baz")); + + expect(() => container.get("foo")).to.throw(`You are attempting to construct 'foo' in a synchronous way + but it has asynchronous dependencies.`); + }); + + it("Should allow onActivation (sync) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); + + container.onActivation("foo", () => "baz"); + + const result = container.get("foo"); + + expect(result).eql("baz"); + }); + + it("Should allow onActivation to replace objects in async autoBindInjectable chain", async () => { + class Level1 { + + } + + @injectable() + class Level2 { + public level1: Level1; + + constructor(@inject(Level1) l1: Level1) { + this.level1 = l1; + } + } + + @injectable() + class Level3 { + public level2: Level2; + + constructor(@inject(Level2) l2: Level2) { + this.level2 = l2; + } + } + + const constructedLevel2 = new Level2(new Level1()); + + const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); + container.bind(Level1).toDynamicValue(() => Promise.resolve(new Level1())); + container.onActivation(Level2, () => { + return Promise.resolve(constructedLevel2); + }); + + const level2 = await container.getAsync(Level2); + + expect(level2).equals(constructedLevel2); + + const level3 = await container.getAsync(Level3); + + expect(level3.level2).equals(constructedLevel2); + }); + + it("Should allow onActivation (async) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); + + container.onActivation("foo", () => Promise.resolve("baz")); + + const result = await container.getAsync("foo"); + + expect(result).eql("baz"); + }); + + it("Should allow onActivation (sync) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + + container.onActivation("foo", () => "baz"); + + const result = await container.getAsync("foo"); + + expect(result).eql("baz"); + }); + + it("Should allow onActivation (async) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + + container.onActivation("foo", () => Promise.resolve("baz")); + + const result = await container.getAsync("foo"); + + expect(result).eql("baz"); + }); + + it("Should allow onActivation (sync) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + + container.onActivation("foo", (context, previous) => `${previous}baz`); + + const result = container.get("foo"); + + expect(result).eql("bumbaz"); + }); + + it("Should allow onActivation (async) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + + const result = await container.getAsync("foo"); + + expect(result).eql("bumbaz"); + }); + + it("Should allow onActivation (sync) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + + container.onActivation("foo", (context, previous) => `${previous}baz`); + + const result = await container.getAsync("foo"); + + expect(result).eql("bumbaz"); + }); + + it("Should allow onActivation (async) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + + const result = await container.getAsync("foo"); + + expect(result).eql("bumbaz"); + }); + + it("Should allow onActivation (sync) of parent (async) through autobind tree", async () => { + class Parent { + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Parent(); + // @ts-ignore + constructed.foo = "bar"; + + container.onActivation(Parent, () => constructed); + + const result = await container.getAsync(Child); + + expect(result.parent).equals(constructed); + }); + + it("Should allow onActivation (sync) of child (async) through autobind tree", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Child(new Parent()); + + container.onActivation(Child, () => constructed); + + const result = await container.getAsync(Child); + + expect(result).equals(constructed); + }); + + it("Should allow onActivation (async) of parent (async) through autobind tree", async () => { + class Parent { + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Parent(); + + container.onActivation(Parent, () => Promise.resolve(constructed)); + + const result = await container.getAsync(Child); + + expect(result.parent).equals(constructed); + }); + + it("Should allow onActivation (async) of child (async) through autobind tree", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Child(new Parent()); + + container.onActivation(Child, () => Promise.resolve(constructed)); + + const result = await container.getAsync(Child); + + expect(result).equals(constructed); + }); + + it("Should allow onActivation of child on parent container", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Child(new Parent()); + + container.onActivation(Child, () => Promise.resolve(constructed)); + + const child = container.createChild(); + + const result = await child.getAsync(Child); + + expect(result).equals(constructed); + }); + + it("Should allow onActivation of parent on parent container", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Parent(); + + container.onActivation(Parent, () => Promise.resolve(constructed)); + + const child = container.createChild(); + + const result = await child.getAsync(Child); + + expect(result.parent).equals(constructed); + }); + + it("Should allow onActivation of child from child container", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + + const constructed = new Child(new Parent()); + + const child = container.createChild(); + child.onActivation(Child, () => Promise.resolve(constructed)); + + const result = await child.getAsync(Child); + + expect(result).equals(constructed); + }); + + it("Should priortize onActivation of parent container over child container", async () => { + const container = new Container(); + container.onActivation("foo", (context, previous) => `${previous}baz`); + container.onActivation("foo", (context, previous) => `${previous}1`); + + const child = container.createChild(); + + child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); + child.onActivation("foo", (context, previous) => `${previous}bum`); + child.onActivation("foo", (context, previous) => `${previous}2`); + + const result = child.get("foo"); + + expect(result).equals("barbahbaz1bum2"); + }); + + it("Should not allow onActivation of parent on child container", async () => { + class Parent { + + } + + @injectable() + class Child { + public parent: Parent; + + public constructor(@inject(Parent)parent: Parent) { + this.parent = parent; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())).inSingletonScope(); + + const constructed = new Parent(); + + const child = container.createChild(); + child.onActivation(Parent, () => Promise.resolve(constructed)); + + const result = await child.getAsync(Child); + + expect(result.parent).not.equals(constructed); + }); + + it("Should wait until onActivation promise resolves before returning object", async () => { + let resolved = false; + + @injectable() + class Constructable { + } + + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + resolved = true; + r(c); + })); + + const result = await container.getAsync("Constructable"); + + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); + + it("Should wait until postConstruct promise resolves before returning object", async () => { + let resolved = false; + + @injectable() + class Constructable { + @postConstruct() + public myPostConstructMethod() { + return new Promise((r) => { + resolved = true; + r(); + }); + } + } + + const container = new Container(); + container.bind("Constructable").to(Constructable); + + const result = await container.getAsync("Constructable"); + + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); + + it("Should only call async method once if marked as singleton (indirect)", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + expect(subject1.doSomething() === subject2.doSomething()).eql(true); + }); + + it("Should support async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); + + this.asyncValue = asyncValue; + } + } + + const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const object1 = await container.getAsync(MixedDependency); + const object2 = await container.getAsync(MixedDependency); + + expect(object1).equals(object2); + }); + + it("Should support shared async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); + + this.asyncValue = asyncValue; + } + } + + const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const async = await container.getAsync(AsyncValue); + + const object1 = await container.getAsync(MixedDependency); + + expect(async).equals(object1.asyncValue); + }); + + it("Should support async dependencies in multiple layers", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + //expect(date).instanceOf(date); + + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue, @inject("Date") date: Date) { + expect(asyncValue).instanceOf(AsyncValue); + expect(date).instanceOf(Date); + + this.date = date; + this.asyncValue = asyncValue; + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const subject1 = await container.getAsync(MixedDependency); + expect(subject1.date).instanceOf(Date); + expect(subject1.asyncValue).instanceOf(AsyncValue); + }); + + it("Should support async values already in cache", async () => { + const container = new Container({autoBindInjectable: true}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + expect(await container.getAsync("Date")).instanceOf(Date); + }); + + it("Should support async values already in cache when there dependencies", async () => { + @injectable() + class HasDependencies { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(HasDependencies); + }); + + it("Should support async values already in cache when there are transient dependencies", async () => { + @injectable() + class Parent { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } + + @injectable() + class Child { + public constructor( + @inject(Parent) parent: Parent, + @inject("Date") date: Date + ) { + expect(parent).instanceOf(Parent); + expect(date).instanceOf(Date); + } + } + + const container = new Container({autoBindInjectable: true}); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(Child); + }); + + it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public foobar: string; + + public constructor(@inject("Date") currentDate: Date, @inject("Static") foobar: string) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + this.foobar = foobar; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + container.bind("Static").toConstantValue("foobar"); + + const subject1 = await container.getAsync("UseDate"); + expect(subject1.foobar).eql("foobar"); + }); + + it("Should throw exception if using sync API with async dependencies", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + + expect(() => container.get("UseDate")).to.throw(`You are attempting to construct 'UseDate' in a synchronous way + but it has asynchronous dependencies.`); + }); + + it("Should be able to resolve indirect Promise bindings", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + // tslint:disable-next-line:no-console + console.log(subject1, subject2); + expect(subject1.doSomething() === subject2.doSomething()).eql(false); + }); + + it("Should be able to resolve direct promise bindings", async () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); + + const value = await container.getAsync("async"); + expect(value).eql("foobar"); + }); + + it("Should error if trying to resolve an promise in sync API", () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); + + expect(() => container.get("async")).to.throw(`You are attempting to construct 'async' in a synchronous way + but it has asynchronous dependencies.`); + }); }); From 3e5b70e9b2935cf9b6df8cfd429318f593823cf0 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 19:09:59 -0400 Subject: [PATCH 04/64] appveyor works again? --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 45fb26c8f..33514f653 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -217,7 +217,7 @@ gulp.task("karma", gulp.series("bundle-test", function (done) { } else { console.log('Browser tests passed'); - process.exit(); // for some reason, AppVeyor hangs, so doing this instead of done(); + done(); } }).start(); })); From 2e14955067defa3a9466433c3ea48cc5023359ba Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 23:26:23 -0400 Subject: [PATCH 05/64] latest versions of FF require latest launcher to not hang --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2dc69e195..4f290d46a 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "karma-chrome-launcher": "2.2.0", "karma-commonjs": "1.0.0", "karma-es6-shim": "1.0.0", - "karma-firefox-launcher": "1.1.0", + "karma-firefox-launcher": "1.2.0", "karma-ie-launcher": "1.0.0", "karma-mocha": "1.3.0", "karma-mocha-reporter": "2.2.5", From b1e4ff8c0826e4dd69c36126c3e93aa5960150dd Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" <23040076+greenkeeper[bot]@users.noreply.github.com> Date: Sun, 30 Jun 2019 17:07:47 +0000 Subject: [PATCH 06/64] chore(package): update publish-please to version 5.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8720dfbbb..2dc69e195 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "karma-sinon": "1.0.5", "mocha": "6.1.4", "performance-now": "2.1.0", - "publish-please": "5.4.3", + "publish-please": "5.5.0", "reflect-metadata": "0.1.13", "run-sequence": "2.2.1", "sinon": "7.3.2", From c1f887dc3bea051cfb09c39c5c7132417c5e9936 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 14:00:37 -0400 Subject: [PATCH 07/64] fix for Microsoft/TSJS-lib-generator#559 --- gulpfile.js | 14 +++++++++++++- package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 9263969ef..41c551a11 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,7 @@ var gulp = require("gulp"), mocha = require("gulp-mocha"), istanbul = require("gulp-istanbul"), karma = require("karma"), + fs = require('fs'), del = require('del'); //****************************************************************************** @@ -102,6 +103,7 @@ gulp.task("build-es", function () { var tsDtsProject = tsc.createProject("tsconfig.json", { declaration: true, noResolve: false, + typescript: require("typescript") }); @@ -226,6 +228,16 @@ if (process.env.APPVEYOR) { gulp.task("test", gulp.series("mocha")); } +gulp.task('verify-dts', function(done){ + const file = fs.readFileSync(`${__dirname}/dts/syntax/constraint_helpers.d.ts`).toString(); + + if(file.includes('TimerHandler')){ + throw new Error('Microsoft/TSJS-lib-generator/issues/559 bug was re-introduced.'); + } + + done(); +}); + //****************************************************************************** //* DEFAULT //****************************************************************************** @@ -234,6 +246,6 @@ gulp.task("build", gulp.series("lint", gulp.parallel( "build-es", "build-lib", "build-amd", - "build-dts"), "build-test")); + "build-dts"), "verify-dts", "build-test")); gulp.task("default", gulp.series("clean", "build", "test")); diff --git a/package.json b/package.json index 2dc69e195..2cbed7036 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "run-sequence": "2.2.1", "sinon": "7.3.2", "tslint": "5.17.0", - "typescript": "^3.5.1", + "typescript": "3.0.3", "vinyl-buffer": "1.0.1", "vinyl-source-stream": "2.0.0" } From 2d7d79466132f4be0e8152ba8d883d501e6a1cb3 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 22 Aug 2019 23:26:23 -0400 Subject: [PATCH 08/64] latest versions of FF require latest launcher to not hang --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cbed7036..b134de35a 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "karma-chrome-launcher": "2.2.0", "karma-commonjs": "1.0.0", "karma-es6-shim": "1.0.0", - "karma-firefox-launcher": "1.1.0", + "karma-firefox-launcher": "1.2.0", "karma-ie-launcher": "1.0.0", "karma-mocha": "1.3.0", "karma-mocha-reporter": "2.2.5", From f1971a476fccec1833913829cf4e9c1cc6ccc0e1 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Fri, 23 Aug 2019 01:38:34 -0400 Subject: [PATCH 09/64] context bug --- src/resolution/resolver.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 29e1b79af..486ba0049 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -156,10 +156,11 @@ function onActivation(request: interfaces.Request, binding: interfaces.Bindin const iter = containers.entries(); - return activationLoop(iter.next().value[1], iter, binding, request.serviceIdentifier, result); + return activationLoop(request.parentContext, iter.next().value[1], iter, binding, request.serviceIdentifier, result); } function activationLoop( + context: interfaces.Context, container: interfaces.Container, containerIterator: IterableIterator<[number, interfaces.Container]>, binding: interfaces.Binding, @@ -168,7 +169,7 @@ function activationLoop( iterator?: IterableIterator<[number, interfaces.BindingActivation]> ): T | Promise { if (previous instanceof Promise) { - return previous.then((unpromised) => activationLoop(container, containerIterator, binding, identifier, unpromised)); + return previous.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); } let result = previous; @@ -185,10 +186,10 @@ function activationLoop( let next = iter.next(); while (!next.done) { - result = next.value[1](this, result); + result = next.value[1](context, result); if (result instanceof Promise) { - return result.then((unpromised) => activationLoop(container, containerIterator, binding, identifier, unpromised, iter)); + return result.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); } next = iter.next(); @@ -198,7 +199,7 @@ function activationLoop( if (nextContainer.value && !getBindingDictionary(container).hasKey(identifier)) { // make sure if we are currently on the container that owns the binding, not to keep looping down to child containers - return activationLoop(nextContainer.value[1], containerIterator, binding, identifier, result); + return activationLoop(context, nextContainer.value[1], containerIterator, binding, identifier, result); } return result; From 40a79519b661263336cbbe19e4087bf8197cab22 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 12 Sep 2019 08:16:01 -0400 Subject: [PATCH 10/64] support promise retying --- src/resolution/resolver.ts | 9 +++++++++ test/resolution/resolver.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 486ba0049..b25677be8 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -117,6 +117,15 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => if (isSingleton) { binding.cache = result; binding.activated = true; + + if (result instanceof Promise) { + result.catch((ex) => { + // allow binding to retry in future + binding.cache = null; + + throw ex; + }); + } } if ( diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 8cbeb4272..d500fbc1c 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1579,6 +1579,33 @@ describe("Resolve", () => { but it has asynchronous dependencies.`); }); + it("Should retry promise if first time failed", async () => { + @injectable() + class Constructable { + } + + let attemped = false; + + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => { + if (attemped) { + return Promise.resolve(new Constructable()); + } + + attemped = true; + + return Promise.reject("break"); + }).inSingletonScope(); + + try { + await container.getAsync("Constructable"); + + throw new Error("should have thrown exception."); + } catch (ex) { + await container.getAsync("Constructable"); + } + }); + it("Should return resolved instance to onActivation when binding is async", async () => { @injectable() class Constructable { From cfe6661f0c4958fafff63fcc80765c79f1e09455 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Sun, 15 Sep 2019 13:28:58 -0400 Subject: [PATCH 11/64] change promise checks to thenable checks --- src/container/container.ts | 23 ++++++++++++----------- src/resolution/instantiation.ts | 7 ++++--- src/resolution/resolver.ts | 23 +++++++++++++---------- src/utils/async.ts | 5 +++++ 4 files changed, 34 insertions(+), 24 deletions(-) create mode 100644 src/utils/async.ts diff --git a/src/container/container.ts b/src/container/container.ts index cef3b41f0..73d96b480 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -7,6 +7,7 @@ import { MetadataReader } from "../planning/metadata_reader"; import { createMockRequest, getBindingDictionary, plan } from "../planning/planner"; import { resolve } from "../resolution/resolver"; import { BindingToSyntax } from "../syntax/binding_to_syntax"; +import { isPromise } from "../utils/async"; import { id } from "../utils/id"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; @@ -174,7 +175,7 @@ class Container implements interfaces.Container { for (const binding of bindings) { const result = this.preDestroy(binding); - if (result instanceof Promise) { + if (isPromise(result)) { throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); } } @@ -209,7 +210,7 @@ class Container implements interfaces.Container { for (const binding of value) { const result = this.preDestroy(binding); - if (result instanceof Promise) { + if (isPromise(result)) { throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); } } @@ -225,8 +226,8 @@ class Container implements interfaces.Container { for (const binding of value) { const result = this.preDestroy(binding); - if (result instanceof Promise) { - promises.push(result); + if (isPromise(result)) { + promises.push(result as Promise); } } }); @@ -381,8 +382,8 @@ class Container implements interfaces.Container { return; } - if (binding.cache instanceof Promise) { - return binding.cache.then(async (resolved) => this.doDeactivation(binding, resolved)); + if (isPromise(binding.cache)) { + return binding.cache.then((resolved: any) => this.doDeactivation(binding, resolved)); } return this.doDeactivation(binding, binding.cache); @@ -411,8 +412,8 @@ class Container implements interfaces.Container { while (deact.value) { const result = deact.value[1](instance); - if (result instanceof Promise) { - return result.then(() => { + if (isPromise(result)) { + return (result as Promise).then(() => { this.doDeactivation(binding, instance, deactivations); }).catch((ex) => { throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); @@ -434,8 +435,8 @@ class Container implements interfaces.Container { if (typeof binding.onDeactivation === "function") { const result = binding.onDeactivation(instance); - if (result instanceof Promise) { - return result.then(() => this.destroyMetadata(constr, instance)); + if (isPromise(result)) { + return (result as Promise).then(() => this.destroyMetadata(constr, instance)); } } @@ -535,7 +536,7 @@ class Container implements interfaces.Container { result = this._planAndResolve()(defaultArgs); } - if (result instanceof Promise && !async) { + if (isPromise(result) && !async) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(serviceIdentifier)); } diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 540e617fa..73b92ca0e 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -3,6 +3,7 @@ import { TargetTypeEnum } from "../constants/literal_types"; import * as METADATA_KEY from "../constants/metadata_keys"; import { interfaces } from "../interfaces/interfaces"; import { Metadata } from "../planning/metadata"; +import { isPromise } from "../utils/async"; function _injectProperties( instance: any, @@ -67,7 +68,7 @@ function resolveInstance( const constructorInjections = constructorInjectionsRequests.map(resolveRequest); - if (constructorInjections.some((ci) => ci instanceof Promise)) { + if (constructorInjections.some(isPromise)) { return new Promise( async (resolve, reject) => { try { const resolved = await Promise.all(constructorInjections); @@ -93,8 +94,8 @@ function resolveInstance( const post = _postConstruct(constr, result); - if (post instanceof Promise) { - return post.then(() => result); + if (isPromise(post)) { + return (post as Promise).then(() => result); } return result; diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index b25677be8..717c4e432 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -2,6 +2,7 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import { BindingScopeEnum, BindingTypeEnum } from "../constants/literal_types"; import { interfaces } from "../interfaces/interfaces"; import { getBindingDictionary } from "../planning/planner"; +import { isPromise } from "../utils/async"; import { isStackOverflowExeption } from "../utils/exceptions"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { resolveInstance } from "./instantiation"; @@ -118,8 +119,8 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => binding.cache = result; binding.activated = true; - if (result instanceof Promise) { - result.catch((ex) => { + if (isPromise(result)) { + (result as Promise).catch((ex) => { // allow binding to retry in future binding.cache = null; @@ -141,16 +142,16 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => }; -function onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T): T | Promise { - if (resolved instanceof Promise) { - return resolved.then((unpromised) => onActivation(request, binding, unpromised)); +function onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T | Promise): T | Promise { + if (isPromise(resolved)) { + return (resolved as Promise).then((unpromised) => onActivation(request, binding, unpromised)); } let result: T | Promise = resolved; // use activation handler if available if (typeof binding.onActivation === "function") { - result = binding.onActivation(request.parentContext, result); + result = binding.onActivation(request.parentContext, result as T); } const containers = [request.parentContext.container]; @@ -177,8 +178,9 @@ function activationLoop( previous: T | Promise, iterator?: IterableIterator<[number, interfaces.BindingActivation]> ): T | Promise { - if (previous instanceof Promise) { - return previous.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); + if (isPromise(previous)) { + return (previous as Promise) + .then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); } let result = previous; @@ -197,8 +199,9 @@ function activationLoop( while (!next.done) { result = next.value[1](context, result); - if (result instanceof Promise) { - return result.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); + if (isPromise(result)) { + return (result as Promise) + .then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); } next = iter.next(); diff --git a/src/utils/async.ts b/src/utils/async.ts new file mode 100644 index 000000000..30c3233e6 --- /dev/null +++ b/src/utils/async.ts @@ -0,0 +1,5 @@ +function isPromise(object: any): boolean { + return object && object.then !== undefined && typeof object.then === "function"; +} + +export { isPromise }; From 1f97c3a56128ae786d1f8c4c0535b7d45e37d18a Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Sun, 15 Sep 2019 17:02:10 -0400 Subject: [PATCH 12/64] fix dangling promise --- src/resolution/resolver.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 717c4e432..9fc900859 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -120,7 +120,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => binding.activated = true; if (isPromise(result)) { - (result as Promise).catch((ex) => { + result = (result as Promise).catch((ex) => { // allow binding to retry in future binding.cache = null; From 3ac344dca5dc9f8c705842df10a551628668f339 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Sun, 15 Sep 2019 20:00:35 -0400 Subject: [PATCH 13/64] take advantage of type narrowing --- src/container/container.ts | 6 +++--- src/resolution/instantiation.ts | 2 +- src/resolution/resolver.ts | 10 ++++------ src/utils/async.ts | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 73d96b480..e265c8f25 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -227,7 +227,7 @@ class Container implements interfaces.Container { const result = this.preDestroy(binding); if (isPromise(result)) { - promises.push(result as Promise); + promises.push(result); } } }); @@ -413,7 +413,7 @@ class Container implements interfaces.Container { const result = deact.value[1](instance); if (isPromise(result)) { - return (result as Promise).then(() => { + return result.then(() => { this.doDeactivation(binding, instance, deactivations); }).catch((ex) => { throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); @@ -436,7 +436,7 @@ class Container implements interfaces.Container { const result = binding.onDeactivation(instance); if (isPromise(result)) { - return (result as Promise).then(() => this.destroyMetadata(constr, instance)); + return result.then(() => this.destroyMetadata(constr, instance)); } } diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 73b92ca0e..18f460324 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -95,7 +95,7 @@ function resolveInstance( const post = _postConstruct(constr, result); if (isPromise(post)) { - return (post as Promise).then(() => result); + return post.then(() => result); } return result; diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 9fc900859..599e30454 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -120,7 +120,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => binding.activated = true; if (isPromise(result)) { - result = (result as Promise).catch((ex) => { + result = result.catch((ex) => { // allow binding to retry in future binding.cache = null; @@ -144,7 +144,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => function onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T | Promise): T | Promise { if (isPromise(resolved)) { - return (resolved as Promise).then((unpromised) => onActivation(request, binding, unpromised)); + return resolved.then((unpromised) => onActivation(request, binding, unpromised)); } let result: T | Promise = resolved; @@ -179,8 +179,7 @@ function activationLoop( iterator?: IterableIterator<[number, interfaces.BindingActivation]> ): T | Promise { if (isPromise(previous)) { - return (previous as Promise) - .then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); + return previous.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); } let result = previous; @@ -200,8 +199,7 @@ function activationLoop( result = next.value[1](context, result); if (isPromise(result)) { - return (result as Promise) - .then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); + return result.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); } next = iter.next(); diff --git a/src/utils/async.ts b/src/utils/async.ts index 30c3233e6..0bc635d21 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -1,4 +1,4 @@ -function isPromise(object: any): boolean { +function isPromise(object: any): object is Promise { return object && object.then !== undefined && typeof object.then === "function"; } From a11d135bbfa318a920173851dbd564f5d851dcf4 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Thu, 26 Sep 2019 09:58:01 -0400 Subject: [PATCH 14/64] export predestroy --- src/inversify.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/inversify.ts b/src/inversify.ts index af502e6de..b23403e52 100644 --- a/src/inversify.ts +++ b/src/inversify.ts @@ -12,6 +12,7 @@ export { unmanaged } from "./annotation/unmanaged"; export { multiInject } from "./annotation/multi_inject"; export { targetName } from "./annotation/target_name"; export { postConstruct } from "./annotation/post_construct"; +export { preDestroy } from "./annotation/pre_destroy"; export { MetadataReader } from "./planning/metadata_reader"; export { id } from "./utils/id"; export { interfaces } from "./interfaces/interfaces"; From 5995443f09c8c9be31bf1b38b9a77b083dfa7930 Mon Sep 17 00:00:00 2001 From: Paris Holley Date: Fri, 6 Dec 2019 12:29:44 -0500 Subject: [PATCH 15/64] fix when promises are rejected and null values end up in the cache --- src/resolution/resolver.ts | 1 + test/resolution/resolver.test.ts | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 599e30454..e659884c2 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -123,6 +123,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => result = result.catch((ex) => { // allow binding to retry in future binding.cache = null; + binding.activated = false; throw ex; }); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index d500fbc1c..0df51838c 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1170,6 +1170,48 @@ describe("Resolve", () => { }); + it("Should not cache bindings if a dependency in the async chain fails", async () => { + let level2Attempts = 0; + + @injectable() + class Level2 { + public value: string; + + public constructor(@inject("level1")value: string) { + level2Attempts += 1; + this.value = value; + } + } + + let level1Attempts = 0; + + const container = new Container({defaultScope: "Singleton", autoBindInjectable: true}); + container.bind("level1").toDynamicValue(async (context) => { + level1Attempts += 1; + + if (level1Attempts === 1) { + throw new Error("first try failed."); + } + + return "foobar"; + }); + container.bind("a").to(Level2); + + try { + await container.getAsync("a"); + + throw new Error("should have failed on first invocation."); + } catch (ex) { + // ignore + } + + const level2 = await container.getAsync("a"); + expect(level2.value).equals("foobar"); + + expect(level1Attempts).equals(2); + expect(level2Attempts).equals(1); + }); + it("Should support async when default scope is singleton", async () => { const container = new Container({defaultScope: "Singleton"}); container.bind("a").toDynamicValue( async () => Math.random()); From 97207c372b6ef6f59e89ffa58f786a4e8343710f Mon Sep 17 00:00:00 2001 From: Podaru Dragos Date: Wed, 3 Mar 2021 21:10:18 +0200 Subject: [PATCH 16/64] fix failing ci --- src/container/lookup.ts | 2 +- test/resolution/resolver.test.ts | 3892 +++++++++++++++--------------- tsconfig.json | 2 +- 3 files changed, 1948 insertions(+), 1948 deletions(-) diff --git a/src/container/lookup.ts b/src/container/lookup.ts index eada79287..d2fdf54b1 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -91,7 +91,7 @@ class Lookup | any> implements interfaces.Loo const copy = new Lookup(); this._map.forEach((value, key) => { - value.forEach((b) => copy.add(key, b.clone ? b.clone() : b)); + value.forEach((b: any) => copy.add(key, b.clone ? b.clone() : b)); }); return copy; diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 0df51838c..4641df97d 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -20,2306 +20,2306 @@ import { resolve } from "../../src/resolution/resolver"; describe("Resolve", () => { - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("Should be able to resolve BindingType.Instance bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface Blade {} - - @injectable() - class KatanaBlade implements Blade {} - - interface Handler {} - - @injectable() - class KatanaHandler implements Handler {} - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: Handler, - @inject(katanaBladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should store singleton type bindings in cache", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface Blade {} - - @injectable() - class KatanaBlade implements Blade {} - - interface Handler {} - - @injectable() - class KatanaHandler implements Handler {} - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: Handler, - @inject(katanaBladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana).inSingletonScope(); // SINGLETON! - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler).inSingletonScope(); // SINGLETON! - - const bindingDictionary = getBindingDictionary(container); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); - const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - - const ninja2 = resolve(context); - expect(ninja2 instanceof Ninja).eql(true); - - expect(bindingDictionary.get(katanaId)[0].cache instanceof Katana).eql(true); - - }); - - it("Should throw when an invalid BindingType is detected", () => { - - interface Katana {} - - @injectable() - class Katana implements Katana {} - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject("Katana") @targetName("katana") katana: Katana, - @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - // container and bindings - const ninjaId = "Ninja"; - const container = new Container(); - container.bind(ninjaId); // IMPORTANT! (Invalid binding) - - // context and plan - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const throwFunction = () => { - resolve(context); - }; - - expect(context.plan.rootRequest.bindings[0].type).eql(BindingTypeEnum.Invalid); - expect(throwFunction).to.throw(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${ninjaId}`); - - }); - - it("Should be able to resolve BindingType.ConstantValue bindings", () => { - - interface KatanaBlade {} - - @injectable() - class KatanaBlade implements KatanaBlade {} + let sandbox: sinon.SinonSandbox; - interface KatanaHandler {} + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("Should be able to resolve BindingType.Instance bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } - @injectable() - class KatanaHandler implements KatanaHandler {} - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: Handler, + @inject(katanaBladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor(handler: KatanaHandler, blade: KatanaBlade) { - this.handler = handler; - this.blade = blade; - } - } + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } - interface Shuriken {} + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } - @injectable() - class Shuriken implements Shuriken {} + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler); - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject("Katana") @targetName("katana") katana: Katana, - @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).toConstantValue(new Katana(new KatanaHandler(), new KatanaBlade())); // IMPORTANT! - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.DynamicValue bindings", () => { - - interface UseDate { - doSomething(): Date; - } - - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue((context: interfaces.Context) => new Date()); - - const subject1 = container.get("UseDate"); - const subject2 = container.get("UseDate"); - expect(subject1.doSomething() === subject2.doSomething()).eql(false); - - container.unbind("Date"); - container.bind("Date").toConstantValue(new Date()); - - const subject3 = container.get("UseDate"); - const subject4 = container.get("UseDate"); - expect(subject3.doSomething() === subject4.doSomething()).eql(true); - - }); - - it("Should be able to resolve BindingType.Constructor bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const newableKatanaId = "Newable"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface KatanaBlade {} - - @injectable() - class KatanaBlade implements KatanaBlade {} - - interface KatanaHandler {} - - @injectable() - class KatanaHandler implements KatanaHandler {} - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, - @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(newableKatanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = new Katana(new KatanaHandler(), new KatanaBlade()); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.Factory bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const swordFactoryId = "Factory"; - const katanaId = "Katana"; - const handlerId = "Handler"; - const bladeId = "Blade"; - - interface Blade {} - - @injectable() - class KatanaBlade implements Blade {} - - interface Handler {} - - @injectable() - class KatanaHandler implements Handler {} - - interface Sword { - handler: Handler; - blade: Blade; - } - - type SwordFactory = () => Sword; - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(handlerId) @targetName("handler") handler: Handler, - @inject(bladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(swordFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = makeKatana(); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(bladeId).to(KatanaBlade); - container.bind(handlerId).to(KatanaHandler); - - container.bind>(swordFactoryId).toFactory((theContext: interfaces.Context) => - () => - theContext.container.get(katanaId)); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve bindings with auto factory", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaFactoryId = "Factory"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface KatanaBlade {} - - @injectable() - class KatanaBlade implements KatanaBlade {} - - interface KatanaHandler {} - - @injectable() - class KatanaHandler implements KatanaHandler {} - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - type SwordFactory = () => Sword; - - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, - @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = makeKatana(); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler); - container.bind>(katanaFactoryId).toAutoFactory(katanaId); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.Provider bindings", (done) => { - - type SwordProvider = () => Promise; - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const swordProviderId = "Provider"; - const swordId = "Sword"; - const handlerId = "Handler"; - const bladeId = "Blade"; - - interface Blade {} - - @injectable() - class KatanaBlade implements Blade {} - - interface Handler {} - - @injectable() - class KatanaHandler implements Handler {} - - interface Sword { - handler: Handler; - blade: Blade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(handlerId) @targetName("handler") handler: Handler, - @inject(bladeId) @targetName("handler") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken {} - - @injectable() - class Shuriken implements Shuriken {} - - interface Warrior { - katana: Katana | null; - katanaProvider: SwordProvider; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana | null; - public katanaProvider: SwordProvider; - public shuriken: Shuriken; - public constructor( - @inject(swordProviderId) @targetName("katanaProvider") katanaProvider: SwordProvider, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = null; - this.katanaProvider = katanaProvider; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(swordId).to(Katana); - container.bind(bladeId).to(KatanaBlade); - container.bind(handlerId).to(KatanaHandler); - - container.bind(swordProviderId).toProvider((theContext: interfaces.Context) => - () => - new Promise((resolveFunc) => { - // Using setTimeout to simulate complex initialization - setTimeout(() => { resolveFunc(theContext.container.get(swordId)); }, 100); - })); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - ninja.katanaProvider().then((katana) => { - ninja.katana = katana; - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - done(); - }); - - }); - - it("Should be able to resolve plans with constraints on tagged targets", () => { - - interface Weapon {} - - @injectable() - class Katana implements Weapon { } - - @injectable() - class Shuriken implements Weapon {} - - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } - - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") @tagged("canThrow", false) katana: Weapon, - @inject("Weapon") @targetName("shuriken") @tagged("canThrow", true) shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const ninjaId = "Ninja"; - const weaponId = "Weapon"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); - container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve plans with constraints on named targets", () => { - - interface Weapon {} - - @injectable() - class Katana implements Weapon {} - - @injectable() - class Shuriken implements Weapon {} - - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } - - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") @named("strong")katana: Weapon, - @inject("Weapon") @targetName("shuriken") @named("weak") shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + }); - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana).whenTargetNamed("strong"); - container.bind(weaponId).to(Shuriken).whenTargetNamed("weak"); + it("Should store singleton type bindings in cache", () => { - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; - const ninja = resolve(context); + interface Blade { } - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + @injectable() + class KatanaBlade implements Blade { } - }); + interface Handler { } - it("Should be able to resolve plans with custom contextual constraints", () => { + @injectable() + class KatanaHandler implements Handler { } - interface Weapon {} + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } - @injectable() - class Katana implements Weapon {} + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: Handler, + @inject(katanaBladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } - @injectable() - class Shuriken implements Weapon {} + interface Shuriken { } - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } + @injectable() + class Shuriken implements Shuriken { } - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") katana: Weapon, - @inject("Weapon") @targetName("shuriken") shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } - const container = new Container(); - container.bind(ninjaId).to(Ninja); + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana).inSingletonScope(); // SINGLETON! + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler).inSingletonScope(); // SINGLETON! - container.bind(weaponId).to(Katana).when((request: interfaces.Request) => - request.target.name.equals("katana")); + const bindingDictionary = getBindingDictionary(container); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - container.bind(weaponId).to(Shuriken).when((request: interfaces.Request) => - request.target.name.equals("shuriken")); + expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); + const ninja = resolve(context); + expect(ninja instanceof Ninja).eql(true); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja2 = resolve(context); + expect(ninja2 instanceof Ninja).eql(true); - const ninja = resolve(context); + expect(bindingDictionary.get(katanaId)[0].cache instanceof Katana).eql(true); - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - }); + }); - it("Should be able to resolve plans with multi-injections", () => { + it("Should throw when an invalid BindingType is detected", () => { - interface Weapon { - name: string; - } + interface Katana { } - @injectable() - class Katana implements Weapon { - public name = "Katana"; - } + @injectable() + class Katana implements Katana { } - @injectable() - class Shuriken implements Weapon { - public name = "Shuriken"; - } + interface Shuriken { } - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } + @injectable() + class Shuriken implements Shuriken { } - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @multiInject("Weapon") @targetName("weapons") weapons: Weapon[] - ) { - this.katana = weapons[0]; - this.shuriken = weapons[1]; - } - } + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") @targetName("katana") katana: Katana, + @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana); - container.bind(weaponId).to(Shuriken); + // container and bindings + const ninjaId = "Ninja"; + const container = new Container(); + container.bind(ninjaId); // IMPORTANT! (Invalid binding) - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + // context and plan + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const throwFunction = () => { + resolve(context); + }; - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(context.plan.rootRequest.bindings[0].type).eql(BindingTypeEnum.Invalid); + expect(throwFunction).to.throw(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${ninjaId}`); - // if only one value is bound to weaponId - const container2 = new Container(); - container2.bind(ninjaId).to(Ninja); - container2.bind(weaponId).to(Katana); + }); - const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); + it("Should be able to resolve BindingType.ConstantValue bindings", () => { - const ninja2 = resolve(context2); + interface KatanaBlade { } - expect(ninja2 instanceof Ninja).eql(true); - expect(ninja2.katana instanceof Katana).eql(true); + @injectable() + class KatanaBlade implements KatanaBlade { } - }); + interface KatanaHandler { } - it("Should be able to resolve plans with activation handlers", () => { + @injectable() + class KatanaHandler implements KatanaHandler { } interface Sword { - use(): void; + handler: KatanaHandler; + blade: KatanaBlade; } @injectable() class Katana implements Sword { - public use() { - return "Used Katana!"; + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor(handler: KatanaHandler, blade: KatanaBlade) { + this.handler = handler; + this.blade = blade; } } + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + interface Warrior { katana: Katana; + shuriken: Shuriken; } @injectable() class Ninja implements Warrior { public katana: Katana; + public shuriken: Shuriken; public constructor( - @inject("Katana") katana: Katana + @inject("Katana") @targetName("katana") katana: Katana, + @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken ) { this.katana = katana; + this.shuriken = shuriken; } } const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; const katanaId = "Katana"; const container = new Container(); container.bind(ninjaId).to(Ninja); - - // This is a global for unit testing but remember - // that it is not a good idea to use globals - const timeTracker: string[] = []; - - container.bind(katanaId).to(Katana).onActivation((theContext: interfaces.Context, katana: Katana) => { - const handler = { - apply(target: any, thisArgument: any, argumentsList: any[]) { - timeTracker.push(`Starting ${target.name} ${new Date().getTime()}`); - const result = target.apply(thisArgument, argumentsList); - timeTracker.push(`Finished ${target.name} ${new Date().getTime()}`); - return result; - } - }; - /// create a proxy for method use() own by katana instance about to be injected - katana.use = new Proxy(katana.use, handler); - return katana; - }); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).toConstantValue(new Katana(new KatanaHandler(), new KatanaBlade())); // IMPORTANT! const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); const ninja = resolve(context); - expect(ninja.katana.use()).eql("Used Katana!"); - expect(Array.isArray(timeTracker)).eql(true); - expect(timeTracker.length).eql(2); + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve BindingType.DynamicValue bindings", () => { - }); + interface UseDate { + doSomething(): Date; + } + + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue((context: interfaces.Context) => new Date()); - it("Should be able to resolve BindingType.Function bindings", () => { + const subject1 = container.get("UseDate"); + const subject2 = container.get("UseDate"); + expect(subject1.doSomething() === subject2.doSomething()).eql(false); - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaFactoryId = "KatanaFactory"; + container.unbind("Date"); + container.bind("Date").toConstantValue(new Date()); - type KatanaFactory = () => Katana; + const subject3 = container.get("UseDate"); + const subject4 = container.get("UseDate"); + expect(subject3.doSomething() === subject4.doSomething()).eql(true); - interface KatanaBlade {} + }); - @injectable() - class KatanaBlade implements KatanaBlade {} + it("Should be able to resolve BindingType.Constructor bindings", () => { - interface KatanaHandler {} + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const newableKatanaId = "Newable"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; - @injectable() - class KatanaHandler implements KatanaHandler {} + interface KatanaBlade { } - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } + @injectable() + class KatanaBlade implements KatanaBlade { } - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor(handler: KatanaHandler, blade: KatanaBlade) { - this.handler = handler; - this.blade = blade; - } - } + interface KatanaHandler { } - interface Shuriken {} + @injectable() + class KatanaHandler implements KatanaHandler { } - @injectable() - class Shuriken implements Shuriken {} + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } - interface Warrior { - katanaFactory: KatanaFactory; - shuriken: Shuriken; - } + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, + @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade + ) { + this.handler = handler; + this.blade = blade; + } + } - @injectable() - class Ninja implements Warrior { - public constructor( - @inject(katanaFactoryId) @targetName("katana") public katanaFactory: KatanaFactory, - @inject(shurikenId) @targetName("shuriken") public shuriken: Shuriken - ) { - } - } + interface Shuriken { } - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); + @injectable() + class Shuriken implements Shuriken { } - const katanaFactoryInstance = function() { - return new Katana(new KatanaHandler(), new KatanaBlade()); - }; + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } - container.bind(katanaFactoryId).toFunction(katanaFactoryInstance); + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(newableKatanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = new Katana(new KatanaHandler(), new KatanaBlade()); // IMPORTANT! + this.shuriken = shuriken; + } + } - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! - const ninja = resolve(context); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - expect(typeof ninja.katanaFactory === "function").eql(true); - expect(ninja.katanaFactory() instanceof Katana).eql(true); - expect(ninja.katanaFactory().handler instanceof KatanaHandler).eql(true); - expect(ninja.katanaFactory().blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); }); - it("Should run the @PostConstruct method", () => { + it("Should be able to resolve BindingType.Factory bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const swordFactoryId = "Factory"; + const katanaId = "Katana"; + const handlerId = "Handler"; + const bladeId = "Blade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } interface Sword { - use(): string; + handler: Handler; + blade: Blade; } + type SwordFactory = () => Sword; + @injectable() class Katana implements Sword { - private useMessage: string; + public handler: Handler; + public blade: Blade; + public constructor( + @inject(handlerId) @targetName("handler") handler: Handler, + @inject(bladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } - public use() { - return this.useMessage; + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(swordFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = makeKatana(); // IMPORTANT! + this.shuriken = shuriken; } + } - @postConstruct() - public postConstruct () { - this.useMessage = "Used Katana!"; + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(bladeId).to(KatanaBlade); + container.bind(handlerId).to(KatanaHandler); + + container.bind>(swordFactoryId).toFactory((theContext: interfaces.Context) => + () => + theContext.container.get(katanaId)); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve bindings with auto factory", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaFactoryId = "Factory"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface KatanaBlade { } + + @injectable() + class KatanaBlade implements KatanaBlade { } + + interface KatanaHandler { } + + @injectable() + class KatanaHandler implements KatanaHandler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + type SwordFactory = () => Sword; + + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, + @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade + ) { + this.handler = handler; + this.blade = blade; } } + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + interface Warrior { katana: Katana; + shuriken: Shuriken; } @injectable() class Ninja implements Warrior { public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = makeKatana(); // IMPORTANT! + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler); + container.bind>(katanaFactoryId).toAutoFactory(katanaId); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve BindingType.Provider bindings", (done) => { + + type SwordProvider = () => Promise; + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const swordProviderId = "Provider"; + const swordId = "Sword"; + const handlerId = "Handler"; + const bladeId = "Blade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: Handler; + blade: Blade; + } + + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(handlerId) @targetName("handler") handler: Handler, + @inject(bladeId) @targetName("handler") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana | null; + katanaProvider: SwordProvider; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana | null; + public katanaProvider: SwordProvider; + public shuriken: Shuriken; + public constructor( + @inject(swordProviderId) @targetName("katanaProvider") katanaProvider: SwordProvider, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = null; + this.katanaProvider = katanaProvider; + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(swordId).to(Katana); + container.bind(bladeId).to(KatanaBlade); + container.bind(handlerId).to(KatanaHandler); + + container.bind(swordProviderId).toProvider((theContext: interfaces.Context) => + () => + new Promise((resolveFunc) => { + // Using setTimeout to simulate complex initialization + setTimeout(() => { resolveFunc(theContext.container.get(swordId)); }, 100); + })); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + ninja.katanaProvider().then((katana) => { + ninja.katana = katana; + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + done(); + }); + + }); + + it("Should be able to resolve plans with constraints on tagged targets", () => { + + interface Weapon { } + + @injectable() + class Katana implements Weapon { } + + @injectable() + class Shuriken implements Weapon { } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") @tagged("canThrow", false) katana: Weapon, + @inject("Weapon") @targetName("shuriken") @tagged("canThrow", true) shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); + container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve plans with constraints on named targets", () => { + + interface Weapon { } + + @injectable() + class Katana implements Weapon { } + + @injectable() + class Shuriken implements Weapon { } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") @named("strong") katana: Weapon, + @inject("Weapon") @targetName("shuriken") @named("weak") shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana).whenTargetNamed("strong"); + container.bind(weaponId).to(Shuriken).whenTargetNamed("weak"); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve plans with custom contextual constraints", () => { + + interface Weapon { } + + @injectable() + class Katana implements Weapon { } + + @injectable() + class Shuriken implements Weapon { } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") katana: Weapon, + @inject("Weapon") @targetName("shuriken") shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + container.bind(weaponId).to(Katana).when((request: interfaces.Request) => + request.target.name.equals("katana")); + + container.bind(weaponId).to(Shuriken).when((request: interfaces.Request) => + request.target.name.equals("shuriken")); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + }); + + it("Should be able to resolve plans with multi-injections", () => { + + interface Weapon { + name: string; + } + + @injectable() + class Katana implements Weapon { + public name = "Katana"; + } + + @injectable() + class Shuriken implements Weapon { + public name = "Shuriken"; + } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @multiInject("Weapon") @targetName("weapons") weapons: Weapon[] + ) { + this.katana = weapons[0]; + this.shuriken = weapons[1]; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana); + container.bind(weaponId).to(Shuriken); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + // if only one value is bound to weaponId + const container2 = new Container(); + container2.bind(ninjaId).to(Ninja); + container2.bind(weaponId).to(Katana); + + const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); + + const ninja2 = resolve(context2); + + expect(ninja2 instanceof Ninja).eql(true); + expect(ninja2.katana instanceof Katana).eql(true); + + }); + + it("Should be able to resolve plans with activation handlers", () => { + + interface Sword { + use(): void; + } + + @injectable() + class Katana implements Sword { + public use() { + return "Used Katana!"; + } + } + + interface Warrior { + katana: Katana; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor( + @inject("Katana") katana: Katana + ) { + this.katana = katana; + } + } + + const ninjaId = "Ninja"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + // This is a global for unit testing but remember + // that it is not a good idea to use globals + const timeTracker: string[] = []; + + container.bind(katanaId).to(Katana).onActivation((theContext: interfaces.Context, katana: Katana) => { + const handler = { + apply(target: any, thisArgument: any, argumentsList: any[]) { + timeTracker.push(`Starting ${target.name} ${new Date().getTime()}`); + const result = target.apply(thisArgument, argumentsList); + timeTracker.push(`Finished ${target.name} ${new Date().getTime()}`); + return result; + } + }; + /// create a proxy for method use() own by katana instance about to be injected + katana.use = new Proxy(katana.use, handler); + return katana; + }); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Katana!"); + expect(Array.isArray(timeTracker)).eql(true); + expect(timeTracker.length).eql(2); + + }); + + it("Should be able to resolve BindingType.Function bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaFactoryId = "KatanaFactory"; + + type KatanaFactory = () => Katana; + + interface KatanaBlade { } + + @injectable() + class KatanaBlade implements KatanaBlade { } + + interface KatanaHandler { } + + @injectable() + class KatanaHandler implements KatanaHandler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor(handler: KatanaHandler, blade: KatanaBlade) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katanaFactory: KatanaFactory; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public constructor( + @inject(katanaFactoryId) @targetName("katana") public katanaFactory: KatanaFactory, + @inject(shurikenId) @targetName("shuriken") public shuriken: Shuriken + ) { + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + + const katanaFactoryInstance = function () { + return new Katana(new KatanaHandler(), new KatanaBlade()); + }; + + container.bind(katanaFactoryId).toFunction(katanaFactoryInstance); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(typeof ninja.katanaFactory === "function").eql(true); + expect(ninja.katanaFactory() instanceof Katana).eql(true); + expect(ninja.katanaFactory().handler instanceof KatanaHandler).eql(true); + expect(ninja.katanaFactory().blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should run the @PostConstruct method", () => { + + interface Sword { + use(): string; + } + + @injectable() + class Katana implements Sword { + private useMessage: string; + + public use() { + return this.useMessage; + } + + @postConstruct() + public postConstruct() { + this.useMessage = "Used Katana!"; + } + } + + interface Warrior { + katana: Katana; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + container.bind(katanaId).to(Katana); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Katana!"); + + }); + + it("Should throw an error if the @postConstruct method throws an error", () => { + + @injectable() + class Katana { + + @postConstruct() + public postConstruct() { + throw new Error("Original Message"); + } + } + + expect(() => resolveInstance({} as interfaces.Binding, Katana, [], () => null)) + .to.throw("@postConstruct error in class Katana: Original Message"); + }); + + it("Should run the @PostConstruct method of parent class", () => { + + interface Weapon { + use(): string; + } + + @injectable() + abstract class Sword implements Weapon { + protected useMessage: string; + + @postConstruct() + public postConstruct() { + this.useMessage = "Used Weapon!"; + } + + public abstract use(): string; + } + + @injectable() + class Katana extends Sword { + public use() { + return this.useMessage; + } + } + + interface Warrior { + katana: Katana; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + container.bind(katanaId).to(Katana); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Weapon!"); + + }); + + it("Should run the @PostConstruct method once in the singleton scope", () => { + let timesCalled = 0; + @injectable() + class Katana { + @postConstruct() + public postConstruct() { + timesCalled++; + } + } + + @injectable() + class Ninja { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + + @injectable() + class Samurai { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const samuraiId = "Samurai"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(samuraiId).to(Samurai); + container.bind(katanaId).to(Katana).inSingletonScope(); + container.get(ninjaId); + container.get(samuraiId); + expect(timesCalled).to.be.equal(1); + + }); + + it("Should not cache bindings if a dependency in the async chain fails", async () => { + let level2Attempts = 0; + + @injectable() + class Level2 { + public value: string; + + public constructor(@inject("level1") value: string) { + level2Attempts += 1; + this.value = value; + } + } + + let level1Attempts = 0; + + const container = new Container({ defaultScope: "Singleton", autoBindInjectable: true }); + container.bind("level1").toDynamicValue(async (context) => { + level1Attempts += 1; + + if (level1Attempts === 1) { + throw new Error("first try failed."); + } + + return "foobar"; + }); + container.bind("a").to(Level2); + + try { + await container.getAsync("a"); + + throw new Error("should have failed on first invocation."); + } catch (ex) { + // ignore + } + + const level2 = await container.getAsync("a"); + expect(level2.value).equals("foobar"); + + expect(level1Attempts).equals(2); + expect(level2Attempts).equals(1); + }); + + it("Should support async when default scope is singleton", async () => { + const container = new Container({ defaultScope: "Singleton" }); + container.bind("a").toDynamicValue(async () => Math.random()); + + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); + + expect(object1).equals(object2); + }); + + it("Should return different values if default singleton scope is overriden by bind", async () => { + const container = new Container({ defaultScope: "Singleton" }); + container.bind("a").toDynamicValue(async () => Math.random()).inTransientScope(); + + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); + + expect(object1).not.equals(object2); + }); + + it("Should only call parent async singleton once within child containers", async () => { + const parent = new Container(); + parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const [subject1, subject2] = await Promise.all([ + parent.getAsync("Parent"), + parent.getAsync("Parent") + ]); + + expect(subject1 === subject2).eql(true); + }); + + it("Should return resolved instance to onDeactivation when binding is async", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() + .onDeactivation((instance) => new Promise((r) => { + expect(instance).instanceof(Destroyable); + r(); + })); + + await container.getAsync("Destroyable"); + + await container.unbindAsync("Destroyable"); + }); + + it("Should wait on deactivation promise before returning unbindAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); + + resolved = true; + })); + + container.get("Destroyable"); + + await container.unbindAsync("Destroyable"); + + expect(resolved).eql(true); + }); + + it("Should wait on predestroy promise before returning unbindAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r({}); + + resolved = true; + }); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + await container.unbindAsync("Destroyable"); + + expect(resolved).eql(true); + }); + + it("Should wait on deactivation promise before returning unbindAllAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); + + resolved = true; + })); + + container.get("Destroyable"); + + await container.unbindAllAsync(); + + expect(resolved).eql(true); + }); + + it("Should wait on predestroy promise before returning unbindAllAsync()", async () => { + let resolved = false; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r({}); + + resolved = true; + }); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + await container.unbindAllAsync(); + + expect(resolved).eql(true); + }); + + it("Should not allow transient construction with async preDestroy", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope(); + + expect(() => container.get("Destroyable")).to + .throw("@preDestroy error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should not allow transient construction with async deactivation", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope() + .onDeactivation(() => Promise.resolve()); + + expect(() => container.get("Destroyable")).to + .throw("onDeactivation() error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should force a class with an async deactivation to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async pre destroy to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async deactivation to use the async unbind api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should invoke destroy in order (all async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve({}); + }); } } - const ninjaId = "Ninja"; - const katanaId = "Katana"; const container = new Container(); - container.bind(ninjaId).to(Ninja); - - container.bind(katanaId).to(Katana); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + container.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + parent = roll; + roll += 1; + presolve(); + }); + }); - const ninja = resolve(context); + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { + binding = roll; + roll += 1; + presolve(); + })); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); - expect(ninja.katana.use()).eql("Used Katana!"); + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); }); - it("Should throw an error if the @postConstruct method throws an error", () => { + it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; @injectable() - class Katana { - - @postConstruct() - public postConstruct() { - throw new Error("Original Message"); + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve({}); + }); } } - expect(() => resolveInstance({} as interfaces.Binding, Katana, [], () => null)) - .to.throw("@postConstruct error in class Katana: Original Message"); - }); - - it("Should run the @PostConstruct method of parent class", () => { + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); - interface Weapon { - use(): string; - } + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); - @injectable() - abstract class Sword implements Weapon { - protected useMessage: string; + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); - @postConstruct() - public postConstruct () { - this.useMessage = "Used Weapon!"; - } + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); - public abstract use(): string; - } + it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; @injectable() - class Katana extends Sword { - public use() { - return this.useMessage; + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + klass = roll; + roll += 1; } } - interface Warrior { - katana: Katana; - } + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + child = roll; + roll += 1; + }); + childContainer.get("Destroyable"); + childContainer.unbind("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should force a class with an async pre destroy to use the async unbind api", async () => { @injectable() - class Ninja implements Warrior { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); } } - const ninjaId = "Ninja"; - const katanaId = "Katana"; const container = new Container(); - container.bind(ninjaId).to(Ninja); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); - container.bind(katanaId).to(Katana); + container.get("Destroyable"); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); - const ninja = resolve(context); + it("Should force a class with an async onActivation to use the async api", async () => { + @injectable() + class Constructable { + } - expect(ninja.katana.use()).eql("Used Weapon!"); + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation(() => Promise.resolve()); + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + but it has asynchronous dependencies.`); }); - it("Should run the @PostConstruct method once in the singleton scope", () => { - let timesCalled = 0; + it("Should force a class with an async post construct to use the async api", async () => { @injectable() - class Katana { + class Constructable { @postConstruct() - public postConstruct () { - timesCalled ++; + public myPostConstructMethod() { + return Promise.resolve(); } } - @injectable() - class Ninja { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } - } + const container = new Container(); + container.bind("Constructable").to(Constructable); + + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + but it has asynchronous dependencies.`); + }); + it("Should retry promise if first time failed", async () => { @injectable() - class Samurai { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } + class Constructable { } - const ninjaId = "Ninja"; - const samuraiId = "Samurai"; - const katanaId = "Katana"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(samuraiId).to(Samurai); - container.bind(katanaId).to(Katana).inSingletonScope(); - container.get(ninjaId); - container.get(samuraiId); - expect(timesCalled).to.be.equal(1); - - }); - it("Should not cache bindings if a dependency in the async chain fails", async () => { - let level2Attempts = 0; + let attemped = false; - @injectable() - class Level2 { - public value: string; + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => { + if (attemped) { + return Promise.resolve(new Constructable()); + } - public constructor(@inject("level1")value: string) { - level2Attempts += 1; - this.value = value; - } - } + attemped = true; - let level1Attempts = 0; + return Promise.reject("break"); + }).inSingletonScope(); - const container = new Container({defaultScope: "Singleton", autoBindInjectable: true}); - container.bind("level1").toDynamicValue(async (context) => { - level1Attempts += 1; + try { + await container.getAsync("Constructable"); - if (level1Attempts === 1) { - throw new Error("first try failed."); + throw new Error("should have thrown exception."); + } catch (ex) { + await container.getAsync("Constructable"); } - - return "foobar"; }); - container.bind("a").to(Level2); - try { - await container.getAsync("a"); - - throw new Error("should have failed on first invocation."); - } catch (ex) { - // ignore - } - - const level2 = await container.getAsync("a"); - expect(level2.value).equals("foobar"); - - expect(level1Attempts).equals(2); - expect(level2Attempts).equals(1); - }); + it("Should return resolved instance to onActivation when binding is async", async () => { + @injectable() + class Constructable { + } - it("Should support async when default scope is singleton", async () => { - const container = new Container({defaultScope: "Singleton"}); - container.bind("a").toDynamicValue( async () => Math.random()); + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + expect(c).instanceof(Constructable); - const object1 = await container.getAsync("a"); - const object2 = await container.getAsync("a"); + r(c); + })); - expect(object1).equals(object2); - }); + await container.getAsync("Constructable"); + }); - it("Should return different values if default singleton scope is overriden by bind", async () => { - const container = new Container({defaultScope: "Singleton"}); - container.bind("a").toDynamicValue( async () => Math.random()).inTransientScope(); + it("Should not allow sync get if an async activation was added to container", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - const object1 = await container.getAsync("a"); - const object2 = await container.getAsync("a"); + container.onActivation("foo", () => Promise.resolve("baz")); - expect(object1).not.equals(object2); - }); + expect(() => container.get("foo")).to.throw(`You are attempting to construct 'foo' in a synchronous way + but it has asynchronous dependencies.`); + }); - it("Should only call parent async singleton once within child containers", async () => { - const parent = new Container(); - parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + it("Should allow onActivation (sync) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - const [subject1, subject2] = await Promise.all([ - parent.getAsync("Parent"), - parent.getAsync("Parent") - ]); + container.onActivation("foo", () => "baz"); - expect(subject1 === subject2).eql(true); - }); + const result = container.get("foo"); - it("Should return resolved instance to onDeactivation when binding is async", async () => { - @injectable() - class Destroyable { - } + expect(result).eql("baz"); + }); - const container = new Container(); - container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() - .onDeactivation((instance) => new Promise((r) => { - expect(instance).instanceof(Destroyable); - r(); - })); + it("Should allow onActivation to replace objects in async autoBindInjectable chain", async () => { + class Level1 { - await container.getAsync("Destroyable"); + } - await container.unbindAsync("Destroyable"); - }); + @injectable() + class Level2 { + public level1: Level1; - it("Should wait on deactivation promise before returning unbindAsync()", async () => { - let resolved = false; + constructor(@inject(Level1) l1: Level1) { + this.level1 = l1; + } + } - @injectable() - class Destroyable { - } + @injectable() + class Level3 { + public level2: Level2; - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => new Promise((r) => { - r(); + constructor(@inject(Level2) l2: Level2) { + this.level2 = l2; + } + } - resolved = true; - })); + const constructedLevel2 = new Level2(new Level1()); - container.get("Destroyable"); + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind(Level1).toDynamicValue(() => Promise.resolve(new Level1())); + container.onActivation(Level2, () => { + return Promise.resolve(constructedLevel2); + }); - await container.unbindAsync("Destroyable"); + const level2 = await container.getAsync(Level2); - expect(resolved).eql(true); - }); + expect(level2).equals(constructedLevel2); - it("Should wait on predestroy promise before returning unbindAsync()", async () => { - let resolved = false; + const level3 = await container.getAsync(Level3); - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((r) => { - r(); + expect(level3.level2).equals(constructedLevel2); + }); - resolved = true; - }); - } - } + it("Should allow onActivation (async) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); + container.onActivation("foo", () => Promise.resolve("baz")); - container.get("Destroyable"); + const result = await container.getAsync("foo"); - await container.unbindAsync("Destroyable"); + expect(result).eql("baz"); + }); - expect(resolved).eql(true); - }); + it("Should allow onActivation (sync) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); - it("Should wait on deactivation promise before returning unbindAllAsync()", async () => { - let resolved = false; + container.onActivation("foo", () => "baz"); - @injectable() - class Destroyable { - } + const result = await container.getAsync("foo"); - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => new Promise((r) => { - r(); + expect(result).eql("baz"); + }); - resolved = true; - })); + it("Should allow onActivation (async) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); - container.get("Destroyable"); + container.onActivation("foo", () => Promise.resolve("baz")); - await container.unbindAllAsync(); + const result = await container.getAsync("foo"); - expect(resolved).eql(true); - }); + expect(result).eql("baz"); + }); - it("Should wait on predestroy promise before returning unbindAllAsync()", async () => { - let resolved = false; + it("Should allow onActivation (sync) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((r) => { - r(); + container.onActivation("foo", (context, previous) => `${previous}baz`); - resolved = true; - }); - } - } + const result = container.get("foo"); - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); + expect(result).eql("bumbaz"); + }); - container.get("Destroyable"); + it("Should allow onActivation (async) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); - await container.unbindAllAsync(); + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); - expect(resolved).eql(true); - }); + const result = await container.getAsync("foo"); - it("Should not allow transient construction with async preDestroy", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inTransientScope(); - - expect(() => container.get("Destroyable")).to - .throw("@preDestroy error in class Destroyable: Class cannot be instantiated in transient scope."); - }); - - it("Should not allow transient construction with async deactivation", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inTransientScope() - .onDeactivation(() => Promise.resolve()); - - expect(() => container.get("Destroyable")).to - .throw("onDeactivation() error in class Destroyable: Class cannot be instantiated in transient scope."); - }); - - it("Should force a class with an async deactivation to use the async unbindAll api", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => Promise.resolve()); - - container.get("Destroyable"); - - expect(() => container.unbindAll()).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async pre destroy to use the async unbindAll api", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); - - container.get("Destroyable"); - - expect(() => container.unbindAll()).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async deactivation to use the async unbind api", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => Promise.resolve()); - - container.get("Destroyable"); - - expect(() => container.unbind("Destroyable")).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should invoke destory in order (all async): child container, parent container, binding, class", async () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((presolve) => { - klass = roll; - roll += 1; - presolve(); - }); - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - parent = roll; - roll += 1; - presolve(); - }); - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { - binding = roll; - roll += 1; - presolve(); - })); - childContainer.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - child = roll; - roll += 1; - presolve(); - }); - }); - - childContainer.get("Destroyable"); - await childContainer.unbindAsync("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((presolve) => { - klass = roll; - roll += 1; - presolve(); - }); - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - parent = roll; - roll += 1; - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { - binding = roll; - roll += 1; - }); - childContainer.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - child = roll; - roll += 1; - presolve(); - }); - }); - - childContainer.get("Destroyable"); - await childContainer.unbindAsync("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - klass = roll; - roll += 1; - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - parent = roll; - roll += 1; - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { - binding = roll; - roll += 1; - }); - childContainer.onDeactivation("Destroyable", () => { - child = roll; - roll += 1; - }); - - childContainer.get("Destroyable"); - childContainer.unbind("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should force a class with an async pre destroy to use the async unbind api", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); - - container.get("Destroyable"); - - expect(() => container.unbind("Destroyable")).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async onActivation to use the async api", async () => { - @injectable() - class Constructable { - } - - const container = new Container(); - container.bind("Constructable").to(Constructable).inSingletonScope() - .onActivation(() => Promise.resolve()); - - expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way - but it has asynchronous dependencies.`); - }); + expect(result).eql("bumbaz"); + }); - it("Should force a class with an async post construct to use the async api", async () => { - @injectable() - class Constructable { - @postConstruct() - public myPostConstructMethod() { - return Promise.resolve(); - } - } + it("Should allow onActivation (sync) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); - const container = new Container(); - container.bind("Constructable").to(Constructable); + container.onActivation("foo", (context, previous) => `${previous}baz`); - expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way - but it has asynchronous dependencies.`); - }); + const result = await container.getAsync("foo"); - it("Should retry promise if first time failed", async () => { - @injectable() - class Constructable { - } + expect(result).eql("bumbaz"); + }); - let attemped = false; + it("Should allow onActivation (async) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); - const container = new Container(); - container.bind("Constructable").toDynamicValue(() => { - if (attemped) { - return Promise.resolve(new Constructable()); - } + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); - attemped = true; + const result = await container.getAsync("foo"); - return Promise.reject("break"); - }).inSingletonScope(); + expect(result).eql("bumbaz"); + }); - try { - await container.getAsync("Constructable"); + it("Should allow onActivation (sync) of parent (async) through autobind tree", async () => { + class Parent { + } - throw new Error("should have thrown exception."); - } catch (ex) { - await container.getAsync("Constructable"); - } - }); + @injectable() + class Child { + public parent: Parent; - it("Should return resolved instance to onActivation when binding is async", async () => { - @injectable() - class Constructable { - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container(); - container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() - .onActivation((context, c) => new Promise((r) => { - expect(c).instanceof(Constructable); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - r(c); - })); + const constructed = new Parent(); + // @ts-ignore + constructed.foo = "bar"; - await container.getAsync("Constructable"); - }); + container.onActivation(Parent, () => constructed); - it("Should not allow sync get if an async activation was added to container", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + const result = await container.getAsync(Child); - container.onActivation("foo", () => Promise.resolve("baz")); + expect(result.parent).equals(constructed); + }); - expect(() => container.get("foo")).to.throw(`You are attempting to construct 'foo' in a synchronous way - but it has asynchronous dependencies.`); - }); + it("Should allow onActivation (sync) of child (async) through autobind tree", async () => { + class Parent { - it("Should allow onActivation (sync) of a previously binded sync object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + } - container.onActivation("foo", () => "baz"); + @injectable() + class Child { + public parent: Parent; - const result = container.get("foo"); + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - expect(result).eql("baz"); - }); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - it("Should allow onActivation to replace objects in async autoBindInjectable chain", async () => { - class Level1 { + const constructed = new Child(new Parent()); - } + container.onActivation(Child, () => constructed); - @injectable() - class Level2 { - public level1: Level1; + const result = await container.getAsync(Child); - constructor(@inject(Level1) l1: Level1) { - this.level1 = l1; - } - } + expect(result).equals(constructed); + }); - @injectable() - class Level3 { - public level2: Level2; + it("Should allow onActivation (async) of parent (async) through autobind tree", async () => { + class Parent { + } - constructor(@inject(Level2) l2: Level2) { - this.level2 = l2; - } - } + @injectable() + class Child { + public parent: Parent; - const constructedLevel2 = new Level2(new Level1()); + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); - container.bind(Level1).toDynamicValue(() => Promise.resolve(new Level1())); - container.onActivation(Level2, () => { - return Promise.resolve(constructedLevel2); - }); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const level2 = await container.getAsync(Level2); + const constructed = new Parent(); - expect(level2).equals(constructedLevel2); + container.onActivation(Parent, () => Promise.resolve(constructed)); - const level3 = await container.getAsync(Level3); + const result = await container.getAsync(Child); - expect(level3.level2).equals(constructedLevel2); - }); + expect(result.parent).equals(constructed); + }); - it("Should allow onActivation (async) of a previously binded sync object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + it("Should allow onActivation (async) of child (async) through autobind tree", async () => { + class Parent { - container.onActivation("foo", () => Promise.resolve("baz")); + } - const result = await container.getAsync("foo"); + @injectable() + class Child { + public parent: Parent; - expect(result).eql("baz"); - }); + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - it("Should allow onActivation (sync) of a previously binded async object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - container.onActivation("foo", () => "baz"); + const constructed = new Child(new Parent()); - const result = await container.getAsync("foo"); + container.onActivation(Child, () => Promise.resolve(constructed)); - expect(result).eql("baz"); - }); + const result = await container.getAsync(Child); - it("Should allow onActivation (async) of a previously binded async object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + expect(result).equals(constructed); + }); - container.onActivation("foo", () => Promise.resolve("baz")); + it("Should allow onActivation of child on parent container", async () => { + class Parent { - const result = await container.getAsync("foo"); + } - expect(result).eql("baz"); - }); + @injectable() + class Child { + public parent: Parent; - it("Should allow onActivation (sync) of a previously binded sync object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - container.onActivation("foo", (context, previous) => `${previous}baz`); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const result = container.get("foo"); + const constructed = new Child(new Parent()); - expect(result).eql("bumbaz"); - }); + container.onActivation(Child, () => Promise.resolve(constructed)); - it("Should allow onActivation (async) of a previously binded sync object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + const child = container.createChild(); - container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + const result = await child.getAsync(Child); - const result = await container.getAsync("foo"); + expect(result).equals(constructed); + }); - expect(result).eql("bumbaz"); - }); + it("Should allow onActivation of parent on parent container", async () => { + class Parent { - it("Should allow onActivation (sync) of a previously binded async object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + } - container.onActivation("foo", (context, previous) => `${previous}baz`); + @injectable() + class Child { + public parent: Parent; - const result = await container.getAsync("foo"); + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - expect(result).eql("bumbaz"); - }); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - it("Should allow onActivation (async) of a previously binded async object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + const constructed = new Parent(); - container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + container.onActivation(Parent, () => Promise.resolve(constructed)); - const result = await container.getAsync("foo"); + const child = container.createChild(); - expect(result).eql("bumbaz"); - }); + const result = await child.getAsync(Child); - it("Should allow onActivation (sync) of parent (async) through autobind tree", async () => { - class Parent { - } + expect(result.parent).equals(constructed); + }); - @injectable() - class Child { - public parent: Parent; + it("Should allow onActivation of child from child container", async () => { + class Parent { - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + } - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + @injectable() + class Child { + public parent: Parent; - const constructed = new Parent(); - // @ts-ignore - constructed.foo = "bar"; + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - container.onActivation(Parent, () => constructed); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const result = await container.getAsync(Child); + const constructed = new Child(new Parent()); - expect(result.parent).equals(constructed); - }); + const child = container.createChild(); + child.onActivation(Child, () => Promise.resolve(constructed)); - it("Should allow onActivation (sync) of child (async) through autobind tree", async () => { - class Parent { + const result = await child.getAsync(Child); - } + expect(result).equals(constructed); + }); - @injectable() - class Child { - public parent: Parent; + it("Should priortize onActivation of parent container over child container", async () => { + const container = new Container(); + container.onActivation("foo", (context, previous) => `${previous}baz`); + container.onActivation("foo", (context, previous) => `${previous}1`); - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + const child = container.createChild(); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); + child.onActivation("foo", (context, previous) => `${previous}bum`); + child.onActivation("foo", (context, previous) => `${previous}2`); - const constructed = new Child(new Parent()); + const result = child.get("foo"); - container.onActivation(Child, () => constructed); + expect(result).equals("barbahbaz1bum2"); + }); - const result = await container.getAsync(Child); + it("Should not allow onActivation of parent on child container", async () => { + class Parent { - expect(result).equals(constructed); - }); + } - it("Should allow onActivation (async) of parent (async) through autobind tree", async () => { - class Parent { - } + @injectable() + class Child { + public parent: Parent; - @injectable() - class Child { - public parent: Parent; + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())).inSingletonScope(); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const constructed = new Parent(); - const constructed = new Parent(); + const child = container.createChild(); + child.onActivation(Parent, () => Promise.resolve(constructed)); - container.onActivation(Parent, () => Promise.resolve(constructed)); + const result = await child.getAsync(Child); - const result = await container.getAsync(Child); + expect(result.parent).not.equals(constructed); + }); - expect(result.parent).equals(constructed); - }); + it("Should wait until onActivation promise resolves before returning object", async () => { + let resolved = false; - it("Should allow onActivation (async) of child (async) through autobind tree", async () => { - class Parent { + @injectable() + class Constructable { + } - } + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + resolved = true; + r(c); + })); - @injectable() - class Child { - public parent: Parent; + const result = await container.getAsync("Constructable"); - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + it("Should wait until postConstruct promise resolves before returning object", async () => { + let resolved = false; - const constructed = new Child(new Parent()); + @injectable() + class Constructable { + @postConstruct() + public myPostConstructMethod() { + return new Promise((r) => { + resolved = true; + r({}); + }); + } + } - container.onActivation(Child, () => Promise.resolve(constructed)); + const container = new Container(); + container.bind("Constructable").to(Constructable); - const result = await container.getAsync(Child); + const result = await container.getAsync("Constructable"); - expect(result).equals(constructed); - }); + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); - it("Should allow onActivation of child on parent container", async () => { - class Parent { + it("Should only call async method once if marked as singleton (indirect)", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); - } + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } - @injectable() - class Child { - public parent: Parent; + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + expect(subject1.doSomething() === subject2.doSomething()).eql(true); + }); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + it("Should support async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } - const constructed = new Child(new Parent()); + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); - container.onActivation(Child, () => Promise.resolve(constructed)); + this.asyncValue = asyncValue; + } + } - const child = container.createChild(); + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - const result = await child.getAsync(Child); + const object1 = await container.getAsync(MixedDependency); + const object2 = await container.getAsync(MixedDependency); - expect(result).equals(constructed); - }); + expect(object1).equals(object2); + }); - it("Should allow onActivation of parent on parent container", async () => { - class Parent { + it("Should support shared async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } - } + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); - @injectable() - class Child { - public parent: Parent; + this.asyncValue = asyncValue; + } + } - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const async = await container.getAsync(AsyncValue); - const constructed = new Parent(); + const object1 = await container.getAsync(MixedDependency); - container.onActivation(Parent, () => Promise.resolve(constructed)); + expect(async).equals(object1.asyncValue); + }); - const child = container.createChild(); + it("Should support async dependencies in multiple layers", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + //expect(date).instanceOf(date); - const result = await child.getAsync(Child); + this.date = date; + } + } - expect(result.parent).equals(constructed); - }); + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue, @inject("Date") date: Date) { + expect(asyncValue).instanceOf(AsyncValue); + expect(date).instanceOf(Date); + + this.date = date; + this.asyncValue = asyncValue; + } + } - it("Should allow onActivation of child from child container", async () => { - class Parent { + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - } + const subject1 = await container.getAsync(MixedDependency); + expect(subject1.date).instanceOf(Date); + expect(subject1.asyncValue).instanceOf(AsyncValue); + }); - @injectable() - class Child { - public parent: Parent; + it("Should support async values already in cache", async () => { + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + expect(await container.getAsync("Date")).instanceOf(Date); + }); - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + it("Should support async values already in cache when there dependencies", async () => { + @injectable() + class HasDependencies { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } - const constructed = new Child(new Parent()); + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - const child = container.createChild(); - child.onActivation(Child, () => Promise.resolve(constructed)); + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(HasDependencies); + }); - const result = await child.getAsync(Child); + it("Should support async values already in cache when there are transient dependencies", async () => { + @injectable() + class Parent { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } - expect(result).equals(constructed); - }); + @injectable() + class Child { + public constructor( + @inject(Parent) parent: Parent, + @inject("Date") date: Date + ) { + expect(parent).instanceOf(Parent); + expect(date).instanceOf(Date); + } + } - it("Should priortize onActivation of parent container over child container", async () => { - const container = new Container(); - container.onActivation("foo", (context, previous) => `${previous}baz`); - container.onActivation("foo", (context, previous) => `${previous}1`); + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - const child = container.createChild(); + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(Child); + }); - child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); - child.onActivation("foo", (context, previous) => `${previous}bum`); - child.onActivation("foo", (context, previous) => `${previous}2`); + it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public foobar: string; - const result = child.get("foo"); + public constructor(@inject("Date") currentDate: Date, @inject("Static") foobar: string) { + expect(currentDate).instanceOf(Date); - expect(result).equals("barbahbaz1bum2"); - }); + this.currentDate = currentDate; + this.foobar = foobar; + } + } - it("Should not allow onActivation of parent on child container", async () => { - class Parent { + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + container.bind("Static").toConstantValue("foobar"); - } + const subject1 = await container.getAsync("UseDate"); + expect(subject1.foobar).eql("foobar"); + }); - @injectable() - class Child { - public parent: Parent; + it("Should throw exception if using sync API with async dependencies", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); - public constructor(@inject(Parent)parent: Parent) { - this.parent = parent; - } - } + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } - const container = new Container({autoBindInjectable: true}); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())).inSingletonScope(); + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - const constructed = new Parent(); + expect(() => container.get("UseDate")).to.throw(`You are attempting to construct 'UseDate' in a synchronous way + but it has asynchronous dependencies.`); + }); - const child = container.createChild(); - child.onActivation(Parent, () => Promise.resolve(constructed)); + it("Should be able to resolve indirect Promise bindings", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); - const result = await child.getAsync(Child); + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } - expect(result.parent).not.equals(constructed); - }); + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + // tslint:disable-next-line:no-console + console.log(subject1, subject2); + expect(subject1.doSomething() === subject2.doSomething()).eql(false); + }); - it("Should wait until onActivation promise resolves before returning object", async () => { - let resolved = false; + it("Should be able to resolve direct promise bindings", async () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - @injectable() - class Constructable { - } + const value = await container.getAsync("async"); + expect(value).eql("foobar"); + }); - const container = new Container(); - container.bind("Constructable").to(Constructable).inSingletonScope() - .onActivation((context, c) => new Promise((r) => { - resolved = true; - r(c); - })); + it("Should error if trying to resolve an promise in sync API", () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - const result = await container.getAsync("Constructable"); - - expect(result).instanceof(Constructable); - expect(resolved).eql(true); - }); - - it("Should wait until postConstruct promise resolves before returning object", async () => { - let resolved = false; - - @injectable() - class Constructable { - @postConstruct() - public myPostConstructMethod() { - return new Promise((r) => { - resolved = true; - r(); - }); - } - } - - const container = new Container(); - container.bind("Constructable").to(Constructable); - - const result = await container.getAsync("Constructable"); - - expect(result).instanceof(Constructable); - expect(resolved).eql(true); - }); - - it("Should only call async method once if marked as singleton (indirect)", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const subject1 = await container.getAsync("UseDate"); - const subject2 = await container.getAsync("UseDate"); - expect(subject1.doSomething() === subject2.doSomething()).eql(true); - }); - - it("Should support async singletons when using autoBindInjectable", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public date: Date; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { - expect(asyncValue).instanceOf(AsyncValue); - - this.asyncValue = asyncValue; - } - } - - const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const object1 = await container.getAsync(MixedDependency); - const object2 = await container.getAsync(MixedDependency); - - expect(object1).equals(object2); - }); - - it("Should support shared async singletons when using autoBindInjectable", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { - expect(asyncValue).instanceOf(AsyncValue); - - this.asyncValue = asyncValue; - } - } - - const container = new Container({autoBindInjectable: true, defaultScope: "Singleton"}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const async = await container.getAsync(AsyncValue); - - const object1 = await container.getAsync(MixedDependency); - - expect(async).equals(object1.asyncValue); - }); - - it("Should support async dependencies in multiple layers", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - //expect(date).instanceOf(date); - - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public date: Date; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue, @inject("Date") date: Date) { - expect(asyncValue).instanceOf(AsyncValue); - expect(date).instanceOf(Date); - - this.date = date; - this.asyncValue = asyncValue; - } - } - - const container = new Container({autoBindInjectable: true}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const subject1 = await container.getAsync(MixedDependency); - expect(subject1.date).instanceOf(Date); - expect(subject1.asyncValue).instanceOf(AsyncValue); - }); - - it("Should support async values already in cache", async () => { - const container = new Container({autoBindInjectable: true}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - expect(await container.getAsync("Date")).instanceOf(Date); - }); - - it("Should support async values already in cache when there dependencies", async () => { - @injectable() - class HasDependencies { - public constructor(@inject("Date") date: Date) { - expect(date).instanceOf(Date); - } - } - - const container = new Container({autoBindInjectable: true}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - await container.getAsync(HasDependencies); - }); - - it("Should support async values already in cache when there are transient dependencies", async () => { - @injectable() - class Parent { - public constructor(@inject("Date") date: Date) { - expect(date).instanceOf(Date); - } - } - - @injectable() - class Child { - public constructor( - @inject(Parent) parent: Parent, - @inject("Date") date: Date - ) { - expect(parent).instanceOf(Parent); - expect(date).instanceOf(Date); - } - } - - const container = new Container({autoBindInjectable: true}); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - await container.getAsync(Child); - }); - - it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public foobar: string; - - public constructor(@inject("Date") currentDate: Date, @inject("Static") foobar: string) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - this.foobar = foobar; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - container.bind("Static").toConstantValue("foobar"); - - const subject1 = await container.getAsync("UseDate"); - expect(subject1.foobar).eql("foobar"); - }); - - it("Should throw exception if using sync API with async dependencies", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - - expect(() => container.get("UseDate")).to.throw(`You are attempting to construct 'UseDate' in a synchronous way + expect(() => container.get("async")).to.throw(`You are attempting to construct 'async' in a synchronous way but it has asynchronous dependencies.`); - }); - - it("Should be able to resolve indirect Promise bindings", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - - const subject1 = await container.getAsync("UseDate"); - const subject2 = await container.getAsync("UseDate"); - // tslint:disable-next-line:no-console - console.log(subject1, subject2); - expect(subject1.doSomething() === subject2.doSomething()).eql(false); - }); - - it("Should be able to resolve direct promise bindings", async () => { - const container = new Container(); - container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - - const value = await container.getAsync("async"); - expect(value).eql("foobar"); - }); - - it("Should error if trying to resolve an promise in sync API", () => { - const container = new Container(); - container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - - expect(() => container.get("async")).to.throw(`You are attempting to construct 'async' in a synchronous way - but it has asynchronous dependencies.`); - }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index b58ead8e6..a3af07e86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,4 +24,4 @@ "noUnusedLocals": true, "strictNullChecks": true } -} +} \ No newline at end of file From 9d745ca46673e0b151bcab91a6d1169a06e9ba7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 6 Apr 2021 19:08:24 +0200 Subject: [PATCH 17/64] test(resolution): remove additional spaces --- test/resolution/resolver.test.ts | 4070 +++++++++++++++--------------- 1 file changed, 2035 insertions(+), 2035 deletions(-) diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 4641df97d..c70be3147 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -20,2306 +20,2306 @@ import { resolve } from "../../src/resolution/resolver"; describe("Resolve", () => { - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it("Should be able to resolve BindingType.Instance bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface Blade { } - - @injectable() - class KatanaBlade implements Blade { } - - interface Handler { } - - @injectable() - class KatanaHandler implements Handler { } - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: Handler, - @inject(katanaBladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should store singleton type bindings in cache", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface Blade { } - - @injectable() - class KatanaBlade implements Blade { } - - interface Handler { } - - @injectable() - class KatanaHandler implements Handler { } - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: Handler, - @inject(katanaBladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana).inSingletonScope(); // SINGLETON! - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler).inSingletonScope(); // SINGLETON! - - const bindingDictionary = getBindingDictionary(container); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); - const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - - const ninja2 = resolve(context); - expect(ninja2 instanceof Ninja).eql(true); - - expect(bindingDictionary.get(katanaId)[0].cache instanceof Katana).eql(true); - - }); - - it("Should throw when an invalid BindingType is detected", () => { - - interface Katana { } - - @injectable() - class Katana implements Katana { } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject("Katana") @targetName("katana") katana: Katana, - @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - // container and bindings - const ninjaId = "Ninja"; - const container = new Container(); - container.bind(ninjaId); // IMPORTANT! (Invalid binding) - - // context and plan - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const throwFunction = () => { - resolve(context); - }; - - expect(context.plan.rootRequest.bindings[0].type).eql(BindingTypeEnum.Invalid); - expect(throwFunction).to.throw(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${ninjaId}`); - - }); - - it("Should be able to resolve BindingType.ConstantValue bindings", () => { - - interface KatanaBlade { } - - @injectable() - class KatanaBlade implements KatanaBlade { } - - interface KatanaHandler { } - - @injectable() - class KatanaHandler implements KatanaHandler { } - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor(handler: KatanaHandler, blade: KatanaBlade) { - this.handler = handler; - this.blade = blade; - } - } + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("Should be able to resolve BindingType.Instance bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: Handler, + @inject(katanaBladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should store singleton type bindings in cache", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: Handler, + @inject(katanaBladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana).inSingletonScope(); // SINGLETON! + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler).inSingletonScope(); // SINGLETON! + + const bindingDictionary = getBindingDictionary(container); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); + const ninja = resolve(context); + expect(ninja instanceof Ninja).eql(true); + + const ninja2 = resolve(context); + expect(ninja2 instanceof Ninja).eql(true); + + expect(bindingDictionary.get(katanaId)[0].cache instanceof Katana).eql(true); + + }); + + it("Should throw when an invalid BindingType is detected", () => { + + interface Katana { } + + @injectable() + class Katana implements Katana { } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") @targetName("katana") katana: Katana, + @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + // container and bindings + const ninjaId = "Ninja"; + const container = new Container(); + container.bind(ninjaId); // IMPORTANT! (Invalid binding) + + // context and plan + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const throwFunction = () => { + resolve(context); + }; + + expect(context.plan.rootRequest.bindings[0].type).eql(BindingTypeEnum.Invalid); + expect(throwFunction).to.throw(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${ninjaId}`); + + }); + + it("Should be able to resolve BindingType.ConstantValue bindings", () => { + + interface KatanaBlade { } + + @injectable() + class KatanaBlade implements KatanaBlade { } + + interface KatanaHandler { } + + @injectable() + class KatanaHandler implements KatanaHandler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor(handler: KatanaHandler, blade: KatanaBlade) { + this.handler = handler; + this.blade = blade; + } + } - interface Shuriken { } + interface Shuriken { } - @injectable() - class Shuriken implements Shuriken { } + @injectable() + class Shuriken implements Shuriken { } - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject("Katana") @targetName("katana") katana: Katana, - @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).toConstantValue(new Katana(new KatanaHandler(), new KatanaBlade())); // IMPORTANT! - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.DynamicValue bindings", () => { - - interface UseDate { - doSomething(): Date; - } - - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue((context: interfaces.Context) => new Date()); - - const subject1 = container.get("UseDate"); - const subject2 = container.get("UseDate"); - expect(subject1.doSomething() === subject2.doSomething()).eql(false); - - container.unbind("Date"); - container.bind("Date").toConstantValue(new Date()); - - const subject3 = container.get("UseDate"); - const subject4 = container.get("UseDate"); - expect(subject3.doSomething() === subject4.doSomething()).eql(true); - - }); - - it("Should be able to resolve BindingType.Constructor bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaId = "Katana"; - const newableKatanaId = "Newable"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface KatanaBlade { } - - @injectable() - class KatanaBlade implements KatanaBlade { } - - interface KatanaHandler { } - - @injectable() - class KatanaHandler implements KatanaHandler { } - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, - @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(newableKatanaId) @targetName("katana") katana: Katana, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = new Katana(new KatanaHandler(), new KatanaBlade()); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.Factory bindings", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const swordFactoryId = "Factory"; - const katanaId = "Katana"; - const handlerId = "Handler"; - const bladeId = "Blade"; - - interface Blade { } - - @injectable() - class KatanaBlade implements Blade { } - - interface Handler { } - - @injectable() - class KatanaHandler implements Handler { } - - interface Sword { - handler: Handler; - blade: Blade; - } - - type SwordFactory = () => Sword; - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(handlerId) @targetName("handler") handler: Handler, - @inject(bladeId) @targetName("blade") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(swordFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = makeKatana(); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(bladeId).to(KatanaBlade); - container.bind(handlerId).to(KatanaHandler); - - container.bind>(swordFactoryId).toFactory((theContext: interfaces.Context) => - () => - theContext.container.get(katanaId)); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve bindings with auto factory", () => { - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaFactoryId = "Factory"; - const katanaId = "Katana"; - const katanaHandlerId = "KatanaHandler"; - const katanaBladeId = "KatanaBlade"; - - interface KatanaBlade { } - - @injectable() - class KatanaBlade implements KatanaBlade { } - - interface KatanaHandler { } - - @injectable() - class KatanaHandler implements KatanaHandler { } - - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } - - type SwordFactory = () => Sword; - - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor( - @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, - @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public shuriken: Shuriken; - public constructor( - @inject(katanaFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = makeKatana(); // IMPORTANT! - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(katanaId).to(Katana); - container.bind(katanaBladeId).to(KatanaBlade); - container.bind(katanaHandlerId).to(KatanaHandler); - container.bind>(katanaFactoryId).toAutoFactory(katanaId); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve BindingType.Provider bindings", (done) => { - - type SwordProvider = () => Promise; - - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const swordProviderId = "Provider"; - const swordId = "Sword"; - const handlerId = "Handler"; - const bladeId = "Blade"; - - interface Blade { } - - @injectable() - class KatanaBlade implements Blade { } - - interface Handler { } - - @injectable() - class KatanaHandler implements Handler { } - - interface Sword { - handler: Handler; - blade: Blade; - } - - @injectable() - class Katana implements Sword { - public handler: Handler; - public blade: Blade; - public constructor( - @inject(handlerId) @targetName("handler") handler: Handler, - @inject(bladeId) @targetName("handler") blade: Blade - ) { - this.handler = handler; - this.blade = blade; - } - } - - interface Shuriken { } - - @injectable() - class Shuriken implements Shuriken { } - - interface Warrior { - katana: Katana | null; - katanaProvider: SwordProvider; - shuriken: Shuriken; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana | null; - public katanaProvider: SwordProvider; - public shuriken: Shuriken; - public constructor( - @inject(swordProviderId) @targetName("katanaProvider") katanaProvider: SwordProvider, - @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken - ) { - this.katana = null; - this.katanaProvider = katanaProvider; - this.shuriken = shuriken; - } - } - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); - container.bind(swordId).to(Katana); - container.bind(bladeId).to(KatanaBlade); - container.bind(handlerId).to(KatanaHandler); - - container.bind(swordProviderId).toProvider((theContext: interfaces.Context) => - () => - new Promise((resolveFunc) => { - // Using setTimeout to simulate complex initialization - setTimeout(() => { resolveFunc(theContext.container.get(swordId)); }, 100); - })); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - ninja.katanaProvider().then((katana) => { - ninja.katana = katana; - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.katana.handler instanceof KatanaHandler).eql(true); - expect(ninja.katana.blade instanceof KatanaBlade).eql(true); - done(); - }); - - }); - - it("Should be able to resolve plans with constraints on tagged targets", () => { - - interface Weapon { } - - @injectable() - class Katana implements Weapon { } - - @injectable() - class Shuriken implements Weapon { } - - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } - - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") @tagged("canThrow", false) katana: Weapon, - @inject("Weapon") @targetName("shuriken") @tagged("canThrow", true) shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const ninjaId = "Ninja"; - const weaponId = "Weapon"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); - container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - - }); - - it("Should be able to resolve plans with constraints on named targets", () => { - - interface Weapon { } - - @injectable() - class Katana implements Weapon { } - - @injectable() - class Shuriken implements Weapon { } - - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } - - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") @named("strong") katana: Weapon, - @inject("Weapon") @targetName("shuriken") @named("weak") shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } - - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject("Katana") @targetName("katana") katana: Katana, + @inject("Shuriken") @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).toConstantValue(new Katana(new KatanaHandler(), new KatanaBlade())); // IMPORTANT! + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve BindingType.DynamicValue bindings", () => { + + interface UseDate { + doSomething(): Date; + } + + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue((context: interfaces.Context) => new Date()); + + const subject1 = container.get("UseDate"); + const subject2 = container.get("UseDate"); + expect(subject1.doSomething() === subject2.doSomething()).eql(false); + + container.unbind("Date"); + container.bind("Date").toConstantValue(new Date()); + + const subject3 = container.get("UseDate"); + const subject4 = container.get("UseDate"); + expect(subject3.doSomething() === subject4.doSomething()).eql(true); + + }); + + it("Should be able to resolve BindingType.Constructor bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaId = "Katana"; + const newableKatanaId = "Newable"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface KatanaBlade { } + + @injectable() + class KatanaBlade implements KatanaBlade { } + + interface KatanaHandler { } + + @injectable() + class KatanaHandler implements KatanaHandler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, + @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(newableKatanaId) @targetName("katana") katana: Katana, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = new Katana(new KatanaHandler(), new KatanaBlade()); // IMPORTANT! + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve BindingType.Factory bindings", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const swordFactoryId = "Factory"; + const katanaId = "Katana"; + const handlerId = "Handler"; + const bladeId = "Blade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: Handler; + blade: Blade; + } + + type SwordFactory = () => Sword; + + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(handlerId) @targetName("handler") handler: Handler, + @inject(bladeId) @targetName("blade") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(swordFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = makeKatana(); // IMPORTANT! + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(bladeId).to(KatanaBlade); + container.bind(handlerId).to(KatanaHandler); + + container.bind>(swordFactoryId).toFactory((theContext: interfaces.Context) => + () => + theContext.container.get(katanaId)); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve bindings with auto factory", () => { + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaFactoryId = "Factory"; + const katanaId = "Katana"; + const katanaHandlerId = "KatanaHandler"; + const katanaBladeId = "KatanaBlade"; + + interface KatanaBlade { } + + @injectable() + class KatanaBlade implements KatanaBlade { } + + interface KatanaHandler { } + + @injectable() + class KatanaHandler implements KatanaHandler { } + + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } + + type SwordFactory = () => Sword; + + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor( + @inject(katanaHandlerId) @targetName("handler") handler: KatanaHandler, + @inject(katanaBladeId) @targetName("blade") blade: KatanaBlade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public shuriken: Shuriken; + public constructor( + @inject(katanaFactoryId) @targetName("makeKatana") makeKatana: SwordFactory, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = makeKatana(); // IMPORTANT! + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(katanaId).to(Katana); + container.bind(katanaBladeId).to(KatanaBlade); + container.bind(katanaHandlerId).to(KatanaHandler); + container.bind>(katanaFactoryId).toAutoFactory(katanaId); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve BindingType.Provider bindings", (done) => { + + type SwordProvider = () => Promise; + + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const swordProviderId = "Provider"; + const swordId = "Sword"; + const handlerId = "Handler"; + const bladeId = "Blade"; + + interface Blade { } + + @injectable() + class KatanaBlade implements Blade { } + + interface Handler { } + + @injectable() + class KatanaHandler implements Handler { } + + interface Sword { + handler: Handler; + blade: Blade; + } + + @injectable() + class Katana implements Sword { + public handler: Handler; + public blade: Blade; + public constructor( + @inject(handlerId) @targetName("handler") handler: Handler, + @inject(bladeId) @targetName("handler") blade: Blade + ) { + this.handler = handler; + this.blade = blade; + } + } + + interface Shuriken { } + + @injectable() + class Shuriken implements Shuriken { } + + interface Warrior { + katana: Katana | null; + katanaProvider: SwordProvider; + shuriken: Shuriken; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana | null; + public katanaProvider: SwordProvider; + public shuriken: Shuriken; + public constructor( + @inject(swordProviderId) @targetName("katanaProvider") katanaProvider: SwordProvider, + @inject(shurikenId) @targetName("shuriken") shuriken: Shuriken + ) { + this.katana = null; + this.katanaProvider = katanaProvider; + this.shuriken = shuriken; + } + } + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); + container.bind(swordId).to(Katana); + container.bind(bladeId).to(KatanaBlade); + container.bind(handlerId).to(KatanaHandler); + + container.bind(swordProviderId).toProvider((theContext: interfaces.Context) => + () => + new Promise((resolveFunc) => { + // Using setTimeout to simulate complex initialization + setTimeout(() => { resolveFunc(theContext.container.get(swordId)); }, 100); + })); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + ninja.katanaProvider().then((katana) => { + ninja.katana = katana; + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.katana.handler instanceof KatanaHandler).eql(true); + expect(ninja.katana.blade instanceof KatanaBlade).eql(true); + done(); + }); + + }); + + it("Should be able to resolve plans with constraints on tagged targets", () => { + + interface Weapon { } + + @injectable() + class Katana implements Weapon { } + + @injectable() + class Shuriken implements Weapon { } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") @tagged("canThrow", false) katana: Weapon, + @inject("Weapon") @targetName("shuriken") @tagged("canThrow", true) shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana).whenTargetTagged("canThrow", false); + container.bind(weaponId).to(Shuriken).whenTargetTagged("canThrow", true); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + }); + + it("Should be able to resolve plans with constraints on named targets", () => { + + interface Weapon { } + + @injectable() + class Katana implements Weapon { } + + @injectable() + class Shuriken implements Weapon { } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") @named("strong") katana: Weapon, + @inject("Weapon") @targetName("shuriken") @named("weak") shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana).whenTargetNamed("strong"); - container.bind(weaponId).to(Shuriken).whenTargetNamed("weak"); + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana).whenTargetNamed("strong"); + container.bind(weaponId).to(Shuriken).whenTargetNamed("weak"); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); - }); + }); - it("Should be able to resolve plans with custom contextual constraints", () => { + it("Should be able to resolve plans with custom contextual constraints", () => { - interface Weapon { } + interface Weapon { } - @injectable() - class Katana implements Weapon { } + @injectable() + class Katana implements Weapon { } - @injectable() - class Shuriken implements Weapon { } + @injectable() + class Shuriken implements Weapon { } - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @inject("Weapon") @targetName("katana") katana: Weapon, - @inject("Weapon") @targetName("shuriken") shuriken: Weapon - ) { - this.katana = katana; - this.shuriken = shuriken; - } - } + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @inject("Weapon") @targetName("katana") katana: Weapon, + @inject("Weapon") @targetName("shuriken") shuriken: Weapon + ) { + this.katana = katana; + this.shuriken = shuriken; + } + } - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + const ninjaId = "Ninja"; + const weaponId = "Weapon"; - const container = new Container(); - container.bind(ninjaId).to(Ninja); + const container = new Container(); + container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana).when((request: interfaces.Request) => - request.target.name.equals("katana")); + container.bind(weaponId).to(Katana).when((request: interfaces.Request) => + request.target.name.equals("katana")); - container.bind(weaponId).to(Shuriken).when((request: interfaces.Request) => - request.target.name.equals("shuriken")); + container.bind(weaponId).to(Shuriken).when((request: interfaces.Request) => + request.target.name.equals("shuriken")); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); - }); + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + }); - it("Should be able to resolve plans with multi-injections", () => { + it("Should be able to resolve plans with multi-injections", () => { - interface Weapon { - name: string; - } - - @injectable() - class Katana implements Weapon { - public name = "Katana"; - } - - @injectable() - class Shuriken implements Weapon { - public name = "Shuriken"; - } - - interface Warrior { - katana: Weapon; - shuriken: Weapon; - } - - @injectable() - class Ninja implements Warrior { - public katana: Weapon; - public shuriken: Weapon; - public constructor( - @multiInject("Weapon") @targetName("weapons") weapons: Weapon[] - ) { - this.katana = weapons[0]; - this.shuriken = weapons[1]; - } - } + interface Weapon { + name: string; + } + + @injectable() + class Katana implements Weapon { + public name = "Katana"; + } + + @injectable() + class Shuriken implements Weapon { + public name = "Shuriken"; + } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @multiInject("Weapon") @targetName("weapons") weapons: Weapon[] + ) { + this.katana = weapons[0]; + this.shuriken = weapons[1]; + } + } - const ninjaId = "Ninja"; - const weaponId = "Weapon"; + const ninjaId = "Ninja"; + const weaponId = "Weapon"; - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(weaponId).to(Katana); - container.bind(weaponId).to(Shuriken); + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).to(Katana); + container.bind(weaponId).to(Shuriken); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - expect(ninja.katana instanceof Katana).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); - // if only one value is bound to weaponId - const container2 = new Container(); - container2.bind(ninjaId).to(Ninja); - container2.bind(weaponId).to(Katana); - - const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); - - const ninja2 = resolve(context2); - - expect(ninja2 instanceof Ninja).eql(true); - expect(ninja2.katana instanceof Katana).eql(true); - - }); - - it("Should be able to resolve plans with activation handlers", () => { - - interface Sword { - use(): void; - } - - @injectable() - class Katana implements Sword { - public use() { - return "Used Katana!"; - } - } + // if only one value is bound to weaponId + const container2 = new Container(); + container2.bind(ninjaId).to(Ninja); + container2.bind(weaponId).to(Katana); + + const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); + + const ninja2 = resolve(context2); + + expect(ninja2 instanceof Ninja).eql(true); + expect(ninja2.katana instanceof Katana).eql(true); + + }); + + it("Should be able to resolve plans with activation handlers", () => { + + interface Sword { + use(): void; + } + + @injectable() + class Katana implements Sword { + public use() { + return "Used Katana!"; + } + } - interface Warrior { - katana: Katana; - } + interface Warrior { + katana: Katana; + } - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public constructor( - @inject("Katana") katana: Katana - ) { - this.katana = katana; - } - } + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor( + @inject("Katana") katana: Katana + ) { + this.katana = katana; + } + } - const ninjaId = "Ninja"; - const katanaId = "Katana"; + const ninjaId = "Ninja"; + const katanaId = "Katana"; - const container = new Container(); - container.bind(ninjaId).to(Ninja); - - // This is a global for unit testing but remember - // that it is not a good idea to use globals - const timeTracker: string[] = []; + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + // This is a global for unit testing but remember + // that it is not a good idea to use globals + const timeTracker: string[] = []; - container.bind(katanaId).to(Katana).onActivation((theContext: interfaces.Context, katana: Katana) => { - const handler = { - apply(target: any, thisArgument: any, argumentsList: any[]) { - timeTracker.push(`Starting ${target.name} ${new Date().getTime()}`); - const result = target.apply(thisArgument, argumentsList); - timeTracker.push(`Finished ${target.name} ${new Date().getTime()}`); - return result; - } - }; - /// create a proxy for method use() own by katana instance about to be injected - katana.use = new Proxy(katana.use, handler); - return katana; - }); + container.bind(katanaId).to(Katana).onActivation((theContext: interfaces.Context, katana: Katana) => { + const handler = { + apply(target: any, thisArgument: any, argumentsList: any[]) { + timeTracker.push(`Starting ${target.name} ${new Date().getTime()}`); + const result = target.apply(thisArgument, argumentsList); + timeTracker.push(`Finished ${target.name} ${new Date().getTime()}`); + return result; + } + }; + /// create a proxy for method use() own by katana instance about to be injected + katana.use = new Proxy(katana.use, handler); + return katana; + }); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); - - expect(ninja.katana.use()).eql("Used Katana!"); - expect(Array.isArray(timeTracker)).eql(true); - expect(timeTracker.length).eql(2); - - }); + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Katana!"); + expect(Array.isArray(timeTracker)).eql(true); + expect(timeTracker.length).eql(2); + + }); - it("Should be able to resolve BindingType.Function bindings", () => { + it("Should be able to resolve BindingType.Function bindings", () => { - const ninjaId = "Ninja"; - const shurikenId = "Shuriken"; - const katanaFactoryId = "KatanaFactory"; + const ninjaId = "Ninja"; + const shurikenId = "Shuriken"; + const katanaFactoryId = "KatanaFactory"; - type KatanaFactory = () => Katana; + type KatanaFactory = () => Katana; - interface KatanaBlade { } + interface KatanaBlade { } - @injectable() - class KatanaBlade implements KatanaBlade { } + @injectable() + class KatanaBlade implements KatanaBlade { } - interface KatanaHandler { } + interface KatanaHandler { } - @injectable() - class KatanaHandler implements KatanaHandler { } + @injectable() + class KatanaHandler implements KatanaHandler { } - interface Sword { - handler: KatanaHandler; - blade: KatanaBlade; - } + interface Sword { + handler: KatanaHandler; + blade: KatanaBlade; + } - @injectable() - class Katana implements Sword { - public handler: KatanaHandler; - public blade: KatanaBlade; - public constructor(handler: KatanaHandler, blade: KatanaBlade) { - this.handler = handler; - this.blade = blade; - } - } + @injectable() + class Katana implements Sword { + public handler: KatanaHandler; + public blade: KatanaBlade; + public constructor(handler: KatanaHandler, blade: KatanaBlade) { + this.handler = handler; + this.blade = blade; + } + } - interface Shuriken { } + interface Shuriken { } - @injectable() - class Shuriken implements Shuriken { } + @injectable() + class Shuriken implements Shuriken { } - interface Warrior { - katanaFactory: KatanaFactory; - shuriken: Shuriken; - } + interface Warrior { + katanaFactory: KatanaFactory; + shuriken: Shuriken; + } - @injectable() - class Ninja implements Warrior { - public constructor( - @inject(katanaFactoryId) @targetName("katana") public katanaFactory: KatanaFactory, - @inject(shurikenId) @targetName("shuriken") public shuriken: Shuriken - ) { - } - } + @injectable() + class Ninja implements Warrior { + public constructor( + @inject(katanaFactoryId) @targetName("katana") public katanaFactory: KatanaFactory, + @inject(shurikenId) @targetName("shuriken") public shuriken: Shuriken + ) { + } + } - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(shurikenId).to(Shuriken); + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(shurikenId).to(Shuriken); - const katanaFactoryInstance = function () { - return new Katana(new KatanaHandler(), new KatanaBlade()); - }; + const katanaFactoryInstance = function () { + return new Katana(new KatanaHandler(), new KatanaBlade()); + }; - container.bind(katanaFactoryId).toFunction(katanaFactoryInstance); + container.bind(katanaFactoryId).toFunction(katanaFactoryInstance); - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolve(context); - expect(ninja instanceof Ninja).eql(true); - expect(typeof ninja.katanaFactory === "function").eql(true); - expect(ninja.katanaFactory() instanceof Katana).eql(true); - expect(ninja.katanaFactory().handler instanceof KatanaHandler).eql(true); - expect(ninja.katanaFactory().blade instanceof KatanaBlade).eql(true); - expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(ninja instanceof Ninja).eql(true); + expect(typeof ninja.katanaFactory === "function").eql(true); + expect(ninja.katanaFactory() instanceof Katana).eql(true); + expect(ninja.katanaFactory().handler instanceof KatanaHandler).eql(true); + expect(ninja.katanaFactory().blade instanceof KatanaBlade).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); - }); + }); - it("Should run the @PostConstruct method", () => { + it("Should run the @PostConstruct method", () => { - interface Sword { - use(): string; - } + interface Sword { + use(): string; + } - @injectable() - class Katana implements Sword { - private useMessage: string; + @injectable() + class Katana implements Sword { + private useMessage: string; - public use() { - return this.useMessage; - } + public use() { + return this.useMessage; + } - @postConstruct() - public postConstruct() { - this.useMessage = "Used Katana!"; - } - } + @postConstruct() + public postConstruct() { + this.useMessage = "Used Katana!"; + } + } - interface Warrior { - katana: Katana; - } + interface Warrior { + katana: Katana; + } - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } - } - const ninjaId = "Ninja"; - const katanaId = "Katana"; + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const katanaId = "Katana"; - const container = new Container(); - container.bind(ninjaId).to(Ninja); - - container.bind(katanaId).to(Katana); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja.katana.use()).eql("Used Katana!"); - - }); - - it("Should throw an error if the @postConstruct method throws an error", () => { - - @injectable() - class Katana { - - @postConstruct() - public postConstruct() { - throw new Error("Original Message"); - } - } - - expect(() => resolveInstance({} as interfaces.Binding, Katana, [], () => null)) - .to.throw("@postConstruct error in class Katana: Original Message"); - }); - - it("Should run the @PostConstruct method of parent class", () => { - - interface Weapon { - use(): string; - } - - @injectable() - abstract class Sword implements Weapon { - protected useMessage: string; - - @postConstruct() - public postConstruct() { - this.useMessage = "Used Weapon!"; - } + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + container.bind(katanaId).to(Katana); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Katana!"); + + }); + + it("Should throw an error if the @postConstruct method throws an error", () => { + + @injectable() + class Katana { + + @postConstruct() + public postConstruct() { + throw new Error("Original Message"); + } + } + + expect(() => resolveInstance({} as interfaces.Binding, Katana, [], () => null)) + .to.throw("@postConstruct error in class Katana: Original Message"); + }); + + it("Should run the @PostConstruct method of parent class", () => { + + interface Weapon { + use(): string; + } + + @injectable() + abstract class Sword implements Weapon { + protected useMessage: string; + + @postConstruct() + public postConstruct() { + this.useMessage = "Used Weapon!"; + } - public abstract use(): string; - } + public abstract use(): string; + } - @injectable() - class Katana extends Sword { - public use() { - return this.useMessage; - } - } - - interface Warrior { - katana: Katana; - } - - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } - } - const ninjaId = "Ninja"; - const katanaId = "Katana"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - - container.bind(katanaId).to(Katana); - - const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - - const ninja = resolve(context); - - expect(ninja.katana.use()).eql("Used Weapon!"); - - }); - - it("Should run the @PostConstruct method once in the singleton scope", () => { - let timesCalled = 0; - @injectable() - class Katana { - @postConstruct() - public postConstruct() { - timesCalled++; - } - } + @injectable() + class Katana extends Sword { + public use() { + return this.useMessage; + } + } + + interface Warrior { + katana: Katana; + } + + @injectable() + class Ninja implements Warrior { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + + container.bind(katanaId).to(Katana); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = resolve(context); + + expect(ninja.katana.use()).eql("Used Weapon!"); + + }); + + it("Should run the @PostConstruct method once in the singleton scope", () => { + let timesCalled = 0; + @injectable() + class Katana { + @postConstruct() + public postConstruct() { + timesCalled++; + } + } - @injectable() - class Ninja { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } - } + @injectable() + class Ninja { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } - @injectable() - class Samurai { - public katana: Katana; - public constructor(@inject("Katana") katana: Katana) { - this.katana = katana; - } - } - const ninjaId = "Ninja"; - const samuraiId = "Samurai"; - const katanaId = "Katana"; - - const container = new Container(); - container.bind(ninjaId).to(Ninja); - container.bind(samuraiId).to(Samurai); - container.bind(katanaId).to(Katana).inSingletonScope(); - container.get(ninjaId); - container.get(samuraiId); - expect(timesCalled).to.be.equal(1); - - }); - - it("Should not cache bindings if a dependency in the async chain fails", async () => { - let level2Attempts = 0; - - @injectable() - class Level2 { - public value: string; - - public constructor(@inject("level1") value: string) { - level2Attempts += 1; - this.value = value; - } - } - - let level1Attempts = 0; - - const container = new Container({ defaultScope: "Singleton", autoBindInjectable: true }); - container.bind("level1").toDynamicValue(async (context) => { - level1Attempts += 1; - - if (level1Attempts === 1) { - throw new Error("first try failed."); - } - - return "foobar"; - }); - container.bind("a").to(Level2); - - try { - await container.getAsync("a"); - - throw new Error("should have failed on first invocation."); - } catch (ex) { - // ignore - } + @injectable() + class Samurai { + public katana: Katana; + public constructor(@inject("Katana") katana: Katana) { + this.katana = katana; + } + } + const ninjaId = "Ninja"; + const samuraiId = "Samurai"; + const katanaId = "Katana"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(samuraiId).to(Samurai); + container.bind(katanaId).to(Katana).inSingletonScope(); + container.get(ninjaId); + container.get(samuraiId); + expect(timesCalled).to.be.equal(1); + + }); + + it("Should not cache bindings if a dependency in the async chain fails", async () => { + let level2Attempts = 0; + + @injectable() + class Level2 { + public value: string; + + public constructor(@inject("level1") value: string) { + level2Attempts += 1; + this.value = value; + } + } + + let level1Attempts = 0; + + const container = new Container({ defaultScope: "Singleton", autoBindInjectable: true }); + container.bind("level1").toDynamicValue(async (context) => { + level1Attempts += 1; + + if (level1Attempts === 1) { + throw new Error("first try failed."); + } + + return "foobar"; + }); + container.bind("a").to(Level2); + + try { + await container.getAsync("a"); + + throw new Error("should have failed on first invocation."); + } catch (ex) { + // ignore + } - const level2 = await container.getAsync("a"); - expect(level2.value).equals("foobar"); + const level2 = await container.getAsync("a"); + expect(level2.value).equals("foobar"); - expect(level1Attempts).equals(2); - expect(level2Attempts).equals(1); - }); + expect(level1Attempts).equals(2); + expect(level2Attempts).equals(1); + }); - it("Should support async when default scope is singleton", async () => { - const container = new Container({ defaultScope: "Singleton" }); - container.bind("a").toDynamicValue(async () => Math.random()); + it("Should support async when default scope is singleton", async () => { + const container = new Container({ defaultScope: "Singleton" }); + container.bind("a").toDynamicValue(async () => Math.random()); - const object1 = await container.getAsync("a"); - const object2 = await container.getAsync("a"); + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); - expect(object1).equals(object2); - }); + expect(object1).equals(object2); + }); - it("Should return different values if default singleton scope is overriden by bind", async () => { - const container = new Container({ defaultScope: "Singleton" }); - container.bind("a").toDynamicValue(async () => Math.random()).inTransientScope(); + it("Should return different values if default singleton scope is overriden by bind", async () => { + const container = new Container({ defaultScope: "Singleton" }); + container.bind("a").toDynamicValue(async () => Math.random()).inTransientScope(); - const object1 = await container.getAsync("a"); - const object2 = await container.getAsync("a"); + const object1 = await container.getAsync("a"); + const object2 = await container.getAsync("a"); - expect(object1).not.equals(object2); - }); + expect(object1).not.equals(object2); + }); - it("Should only call parent async singleton once within child containers", async () => { - const parent = new Container(); - parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + it("Should only call parent async singleton once within child containers", async () => { + const parent = new Container(); + parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - const [subject1, subject2] = await Promise.all([ - parent.getAsync("Parent"), - parent.getAsync("Parent") - ]); + const [subject1, subject2] = await Promise.all([ + parent.getAsync("Parent"), + parent.getAsync("Parent") + ]); - expect(subject1 === subject2).eql(true); - }); + expect(subject1 === subject2).eql(true); + }); - it("Should return resolved instance to onDeactivation when binding is async", async () => { - @injectable() - class Destroyable { - } + it("Should return resolved instance to onDeactivation when binding is async", async () => { + @injectable() + class Destroyable { + } - const container = new Container(); - container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() - .onDeactivation((instance) => new Promise((r) => { - expect(instance).instanceof(Destroyable); - r(); - })); + const container = new Container(); + container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() + .onDeactivation((instance) => new Promise((r) => { + expect(instance).instanceof(Destroyable); + r(); + })); - await container.getAsync("Destroyable"); + await container.getAsync("Destroyable"); - await container.unbindAsync("Destroyable"); - }); + await container.unbindAsync("Destroyable"); + }); - it("Should wait on deactivation promise before returning unbindAsync()", async () => { - let resolved = false; + it("Should wait on deactivation promise before returning unbindAsync()", async () => { + let resolved = false; - @injectable() - class Destroyable { - } + @injectable() + class Destroyable { + } - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => new Promise((r) => { - r(); + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); - resolved = true; - })); + resolved = true; + })); - container.get("Destroyable"); + container.get("Destroyable"); - await container.unbindAsync("Destroyable"); + await container.unbindAsync("Destroyable"); - expect(resolved).eql(true); - }); + expect(resolved).eql(true); + }); - it("Should wait on predestroy promise before returning unbindAsync()", async () => { - let resolved = false; + it("Should wait on predestroy promise before returning unbindAsync()", async () => { + let resolved = false; - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((r) => { - r({}); + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r({}); - resolved = true; - }); - } - } + resolved = true; + }); + } + } - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); - container.get("Destroyable"); + container.get("Destroyable"); - await container.unbindAsync("Destroyable"); + await container.unbindAsync("Destroyable"); - expect(resolved).eql(true); - }); + expect(resolved).eql(true); + }); - it("Should wait on deactivation promise before returning unbindAllAsync()", async () => { - let resolved = false; + it("Should wait on deactivation promise before returning unbindAllAsync()", async () => { + let resolved = false; - @injectable() - class Destroyable { - } + @injectable() + class Destroyable { + } - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => new Promise((r) => { - r(); + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => new Promise((r) => { + r(); - resolved = true; - })); + resolved = true; + })); - container.get("Destroyable"); + container.get("Destroyable"); - await container.unbindAllAsync(); + await container.unbindAllAsync(); - expect(resolved).eql(true); - }); + expect(resolved).eql(true); + }); - it("Should wait on predestroy promise before returning unbindAllAsync()", async () => { - let resolved = false; + it("Should wait on predestroy promise before returning unbindAllAsync()", async () => { + let resolved = false; - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((r) => { - r({}); + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((r) => { + r({}); - resolved = true; - }); - } - } + resolved = true; + }); + } + } - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); - container.get("Destroyable"); + container.get("Destroyable"); - await container.unbindAllAsync(); + await container.unbindAllAsync(); - expect(resolved).eql(true); - }); + expect(resolved).eql(true); + }); - it("Should not allow transient construction with async preDestroy", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inTransientScope(); - - expect(() => container.get("Destroyable")).to - .throw("@preDestroy error in class Destroyable: Class cannot be instantiated in transient scope."); - }); - - it("Should not allow transient construction with async deactivation", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inTransientScope() - .onDeactivation(() => Promise.resolve()); - - expect(() => container.get("Destroyable")).to - .throw("onDeactivation() error in class Destroyable: Class cannot be instantiated in transient scope."); - }); - - it("Should force a class with an async deactivation to use the async unbindAll api", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => Promise.resolve()); - - container.get("Destroyable"); - - expect(() => container.unbindAll()).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async pre destroy to use the async unbindAll api", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); - - container.get("Destroyable"); - - expect(() => container.unbindAll()).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async deactivation to use the async unbind api", async () => { - @injectable() - class Destroyable { - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope() - .onDeactivation(() => Promise.resolve()); - - container.get("Destroyable"); - - expect(() => container.unbind("Destroyable")).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should invoke destroy in order (all async): child container, parent container, binding, class", async () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((presolve) => { - klass = roll; - roll += 1; - presolve({}); - }); - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - parent = roll; - roll += 1; - presolve(); - }); - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { - binding = roll; - roll += 1; - presolve(); - })); - childContainer.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - child = roll; - roll += 1; - presolve(); - }); - }); - - childContainer.get("Destroyable"); - await childContainer.unbindAsync("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return new Promise((presolve) => { - klass = roll; - roll += 1; - presolve({}); - }); - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - parent = roll; - roll += 1; - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { - binding = roll; - roll += 1; - }); - childContainer.onDeactivation("Destroyable", () => { - return new Promise((presolve) => { - child = roll; - roll += 1; - presolve(); - }); - }); - - childContainer.get("Destroyable"); - await childContainer.unbindAsync("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { - let roll = 1; - let binding = null; - let klass = null; - let parent = null; - let child = null; - - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - klass = roll; - roll += 1; - } - } - - const container = new Container(); - container.onDeactivation("Destroyable", () => { - parent = roll; - roll += 1; - }); - - const childContainer = container.createChild(); - childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { - binding = roll; - roll += 1; - }); - childContainer.onDeactivation("Destroyable", () => { - child = roll; - roll += 1; - }); - - childContainer.get("Destroyable"); - childContainer.unbind("Destroyable"); - - expect(roll).eql(5); - expect(child).eql(1); - expect(parent).eql(2); - expect(binding).eql(3); - expect(klass).eql(4); - }); - - it("Should force a class with an async pre destroy to use the async unbind api", async () => { - @injectable() - class Destroyable { - @preDestroy() - public myPreDestroyMethod() { - return Promise.resolve(); - } - } - - const container = new Container(); - container.bind("Destroyable").to(Destroyable).inSingletonScope(); - - container.get("Destroyable"); - - expect(() => container.unbind("Destroyable")).to - .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); - }); - - it("Should force a class with an async onActivation to use the async api", async () => { - @injectable() - class Constructable { - } - - const container = new Container(); - container.bind("Constructable").to(Constructable).inSingletonScope() - .onActivation(() => Promise.resolve()); - - expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + it("Should not allow transient construction with async preDestroy", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope(); + + expect(() => container.get("Destroyable")).to + .throw("@preDestroy error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should not allow transient construction with async deactivation", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inTransientScope() + .onDeactivation(() => Promise.resolve()); + + expect(() => container.get("Destroyable")).to + .throw("onDeactivation() error in class Destroyable: Class cannot be instantiated in transient scope."); + }); + + it("Should force a class with an async deactivation to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async pre destroy to use the async unbindAll api", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + expect(() => container.unbindAll()).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async deactivation to use the async unbind api", async () => { + @injectable() + class Destroyable { + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.resolve()); + + container.get("Destroyable"); + + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should invoke destroy in order (all async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve({}); + }); + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + parent = roll; + roll += 1; + presolve(); + }); + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { + binding = roll; + roll += 1; + presolve(); + })); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); + + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve({}); + }); + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); + }); + + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + klass = roll; + roll += 1; + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + child = roll; + roll += 1; + }); + + childContainer.get("Destroyable"); + childContainer.unbind("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); + }); + + it("Should force a class with an async pre destroy to use the async unbind api", async () => { + @injectable() + class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return Promise.resolve(); + } + } + + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope(); + + container.get("Destroyable"); + + expect(() => container.unbind("Destroyable")).to + .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); + }); + + it("Should force a class with an async onActivation to use the async api", async () => { + @injectable() + class Constructable { + } + + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation(() => Promise.resolve()); + + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way but it has asynchronous dependencies.`); - }); + }); - it("Should force a class with an async post construct to use the async api", async () => { - @injectable() - class Constructable { - @postConstruct() - public myPostConstructMethod() { - return Promise.resolve(); - } - } + it("Should force a class with an async post construct to use the async api", async () => { + @injectable() + class Constructable { + @postConstruct() + public myPostConstructMethod() { + return Promise.resolve(); + } + } - const container = new Container(); - container.bind("Constructable").to(Constructable); + const container = new Container(); + container.bind("Constructable").to(Constructable); - expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way + expect(() => container.get("Constructable")).to.throw(`You are attempting to construct 'Constructable' in a synchronous way but it has asynchronous dependencies.`); - }); + }); - it("Should retry promise if first time failed", async () => { - @injectable() - class Constructable { - } + it("Should retry promise if first time failed", async () => { + @injectable() + class Constructable { + } - let attemped = false; + let attemped = false; - const container = new Container(); - container.bind("Constructable").toDynamicValue(() => { - if (attemped) { - return Promise.resolve(new Constructable()); - } + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => { + if (attemped) { + return Promise.resolve(new Constructable()); + } - attemped = true; + attemped = true; - return Promise.reject("break"); - }).inSingletonScope(); + return Promise.reject("break"); + }).inSingletonScope(); - try { - await container.getAsync("Constructable"); + try { + await container.getAsync("Constructable"); - throw new Error("should have thrown exception."); - } catch (ex) { - await container.getAsync("Constructable"); - } - }); + throw new Error("should have thrown exception."); + } catch (ex) { + await container.getAsync("Constructable"); + } + }); - it("Should return resolved instance to onActivation when binding is async", async () => { - @injectable() - class Constructable { - } + it("Should return resolved instance to onActivation when binding is async", async () => { + @injectable() + class Constructable { + } - const container = new Container(); - container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() - .onActivation((context, c) => new Promise((r) => { - expect(c).instanceof(Constructable); + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + expect(c).instanceof(Constructable); - r(c); - })); + r(c); + })); - await container.getAsync("Constructable"); - }); + await container.getAsync("Constructable"); + }); - it("Should not allow sync get if an async activation was added to container", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + it("Should not allow sync get if an async activation was added to container", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - container.onActivation("foo", () => Promise.resolve("baz")); + container.onActivation("foo", () => Promise.resolve("baz")); - expect(() => container.get("foo")).to.throw(`You are attempting to construct 'foo' in a synchronous way + expect(() => container.get("foo")).to.throw(`You are attempting to construct 'foo' in a synchronous way but it has asynchronous dependencies.`); - }); + }); - it("Should allow onActivation (sync) of a previously binded sync object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + it("Should allow onActivation (sync) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - container.onActivation("foo", () => "baz"); + container.onActivation("foo", () => "baz"); - const result = container.get("foo"); + const result = container.get("foo"); - expect(result).eql("baz"); - }); + expect(result).eql("baz"); + }); - it("Should allow onActivation to replace objects in async autoBindInjectable chain", async () => { - class Level1 { + it("Should allow onActivation to replace objects in async autoBindInjectable chain", async () => { + class Level1 { - } + } - @injectable() - class Level2 { - public level1: Level1; + @injectable() + class Level2 { + public level1: Level1; - constructor(@inject(Level1) l1: Level1) { - this.level1 = l1; - } - } + constructor(@inject(Level1) l1: Level1) { + this.level1 = l1; + } + } - @injectable() - class Level3 { - public level2: Level2; + @injectable() + class Level3 { + public level2: Level2; - constructor(@inject(Level2) l2: Level2) { - this.level2 = l2; - } - } + constructor(@inject(Level2) l2: Level2) { + this.level2 = l2; + } + } - const constructedLevel2 = new Level2(new Level1()); + const constructedLevel2 = new Level2(new Level1()); - const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); - container.bind(Level1).toDynamicValue(() => Promise.resolve(new Level1())); - container.onActivation(Level2, () => { - return Promise.resolve(constructedLevel2); - }); + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind(Level1).toDynamicValue(() => Promise.resolve(new Level1())); + container.onActivation(Level2, () => { + return Promise.resolve(constructedLevel2); + }); - const level2 = await container.getAsync(Level2); + const level2 = await container.getAsync(Level2); - expect(level2).equals(constructedLevel2); + expect(level2).equals(constructedLevel2); - const level3 = await container.getAsync(Level3); + const level3 = await container.getAsync(Level3); - expect(level3.level2).equals(constructedLevel2); - }); + expect(level3.level2).equals(constructedLevel2); + }); - it("Should allow onActivation (async) of a previously binded sync object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar"); + it("Should allow onActivation (async) of a previously binded sync object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar"); - container.onActivation("foo", () => Promise.resolve("baz")); + container.onActivation("foo", () => Promise.resolve("baz")); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("baz"); - }); + expect(result).eql("baz"); + }); - it("Should allow onActivation (sync) of a previously binded async object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + it("Should allow onActivation (sync) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); - container.onActivation("foo", () => "baz"); + container.onActivation("foo", () => "baz"); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("baz"); - }); + expect(result).eql("baz"); + }); - it("Should allow onActivation (async) of a previously binded async object (without activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); + it("Should allow onActivation (async) of a previously binded async object (without activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")); - container.onActivation("foo", () => Promise.resolve("baz")); + container.onActivation("foo", () => Promise.resolve("baz")); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("baz"); - }); + expect(result).eql("baz"); + }); - it("Should allow onActivation (sync) of a previously binded sync object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + it("Should allow onActivation (sync) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); - container.onActivation("foo", (context, previous) => `${previous}baz`); + container.onActivation("foo", (context, previous) => `${previous}baz`); - const result = container.get("foo"); + const result = container.get("foo"); - expect(result).eql("bumbaz"); - }); + expect(result).eql("bumbaz"); + }); - it("Should allow onActivation (async) of a previously binded sync object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); + it("Should allow onActivation (async) of a previously binded sync object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toConstantValue("bar").onActivation(() => "bum"); - container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("bumbaz"); - }); + expect(result).eql("bumbaz"); + }); - it("Should allow onActivation (sync) of a previously binded async object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + it("Should allow onActivation (sync) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); - container.onActivation("foo", (context, previous) => `${previous}baz`); + container.onActivation("foo", (context, previous) => `${previous}baz`); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("bumbaz"); - }); + expect(result).eql("bumbaz"); + }); - it("Should allow onActivation (async) of a previously binded async object (with activation)", async () => { - const container = new Container(); - container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); + it("Should allow onActivation (async) of a previously binded async object (with activation)", async () => { + const container = new Container(); + container.bind("foo").toDynamicValue(() => Promise.resolve("bar")).onActivation(() => "bum"); - container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); + container.onActivation("foo", (context, previous) => Promise.resolve(`${previous}baz`)); - const result = await container.getAsync("foo"); + const result = await container.getAsync("foo"); - expect(result).eql("bumbaz"); - }); + expect(result).eql("bumbaz"); + }); - it("Should allow onActivation (sync) of parent (async) through autobind tree", async () => { - class Parent { - } + it("Should allow onActivation (sync) of parent (async) through autobind tree", async () => { + class Parent { + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Parent(); - // @ts-ignore - constructed.foo = "bar"; + const constructed = new Parent(); + // @ts-ignore + constructed.foo = "bar"; - container.onActivation(Parent, () => constructed); + container.onActivation(Parent, () => constructed); - const result = await container.getAsync(Child); + const result = await container.getAsync(Child); - expect(result.parent).equals(constructed); - }); + expect(result.parent).equals(constructed); + }); - it("Should allow onActivation (sync) of child (async) through autobind tree", async () => { - class Parent { + it("Should allow onActivation (sync) of child (async) through autobind tree", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Child(new Parent()); + const constructed = new Child(new Parent()); - container.onActivation(Child, () => constructed); + container.onActivation(Child, () => constructed); - const result = await container.getAsync(Child); + const result = await container.getAsync(Child); - expect(result).equals(constructed); - }); + expect(result).equals(constructed); + }); - it("Should allow onActivation (async) of parent (async) through autobind tree", async () => { - class Parent { - } + it("Should allow onActivation (async) of parent (async) through autobind tree", async () => { + class Parent { + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Parent(); + const constructed = new Parent(); - container.onActivation(Parent, () => Promise.resolve(constructed)); + container.onActivation(Parent, () => Promise.resolve(constructed)); - const result = await container.getAsync(Child); + const result = await container.getAsync(Child); - expect(result.parent).equals(constructed); - }); + expect(result.parent).equals(constructed); + }); - it("Should allow onActivation (async) of child (async) through autobind tree", async () => { - class Parent { + it("Should allow onActivation (async) of child (async) through autobind tree", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Child(new Parent()); + const constructed = new Child(new Parent()); - container.onActivation(Child, () => Promise.resolve(constructed)); + container.onActivation(Child, () => Promise.resolve(constructed)); - const result = await container.getAsync(Child); + const result = await container.getAsync(Child); - expect(result).equals(constructed); - }); + expect(result).equals(constructed); + }); - it("Should allow onActivation of child on parent container", async () => { - class Parent { + it("Should allow onActivation of child on parent container", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Child(new Parent()); + const constructed = new Child(new Parent()); - container.onActivation(Child, () => Promise.resolve(constructed)); + container.onActivation(Child, () => Promise.resolve(constructed)); - const child = container.createChild(); + const child = container.createChild(); - const result = await child.getAsync(Child); + const result = await child.getAsync(Child); - expect(result).equals(constructed); - }); + expect(result).equals(constructed); + }); - it("Should allow onActivation of parent on parent container", async () => { - class Parent { + it("Should allow onActivation of parent on parent container", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Parent(); + const constructed = new Parent(); - container.onActivation(Parent, () => Promise.resolve(constructed)); + container.onActivation(Parent, () => Promise.resolve(constructed)); - const child = container.createChild(); + const child = container.createChild(); - const result = await child.getAsync(Child); + const result = await child.getAsync(Child); - expect(result.parent).equals(constructed); - }); + expect(result.parent).equals(constructed); + }); - it("Should allow onActivation of child from child container", async () => { - class Parent { + it("Should allow onActivation of child from child container", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())); - const constructed = new Child(new Parent()); + const constructed = new Child(new Parent()); - const child = container.createChild(); - child.onActivation(Child, () => Promise.resolve(constructed)); + const child = container.createChild(); + child.onActivation(Child, () => Promise.resolve(constructed)); - const result = await child.getAsync(Child); + const result = await child.getAsync(Child); - expect(result).equals(constructed); - }); + expect(result).equals(constructed); + }); - it("Should priortize onActivation of parent container over child container", async () => { - const container = new Container(); - container.onActivation("foo", (context, previous) => `${previous}baz`); - container.onActivation("foo", (context, previous) => `${previous}1`); + it("Should priortize onActivation of parent container over child container", async () => { + const container = new Container(); + container.onActivation("foo", (context, previous) => `${previous}baz`); + container.onActivation("foo", (context, previous) => `${previous}1`); - const child = container.createChild(); + const child = container.createChild(); - child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); - child.onActivation("foo", (context, previous) => `${previous}bum`); - child.onActivation("foo", (context, previous) => `${previous}2`); + child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); + child.onActivation("foo", (context, previous) => `${previous}bum`); + child.onActivation("foo", (context, previous) => `${previous}2`); - const result = child.get("foo"); + const result = child.get("foo"); - expect(result).equals("barbahbaz1bum2"); - }); + expect(result).equals("barbahbaz1bum2"); + }); - it("Should not allow onActivation of parent on child container", async () => { - class Parent { + it("Should not allow onActivation of parent on child container", async () => { + class Parent { - } + } - @injectable() - class Child { - public parent: Parent; + @injectable() + class Child { + public parent: Parent; - public constructor(@inject(Parent) parent: Parent) { - this.parent = parent; - } - } + public constructor(@inject(Parent) parent: Parent) { + this.parent = parent; + } + } - const container = new Container({ autoBindInjectable: true }); - container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())).inSingletonScope(); + const container = new Container({ autoBindInjectable: true }); + container.bind(Parent).toDynamicValue(() => Promise.resolve(new Parent())).inSingletonScope(); - const constructed = new Parent(); + const constructed = new Parent(); - const child = container.createChild(); - child.onActivation(Parent, () => Promise.resolve(constructed)); + const child = container.createChild(); + child.onActivation(Parent, () => Promise.resolve(constructed)); - const result = await child.getAsync(Child); + const result = await child.getAsync(Child); - expect(result.parent).not.equals(constructed); - }); + expect(result.parent).not.equals(constructed); + }); - it("Should wait until onActivation promise resolves before returning object", async () => { - let resolved = false; + it("Should wait until onActivation promise resolves before returning object", async () => { + let resolved = false; - @injectable() - class Constructable { - } + @injectable() + class Constructable { + } - const container = new Container(); - container.bind("Constructable").to(Constructable).inSingletonScope() - .onActivation((context, c) => new Promise((r) => { - resolved = true; - r(c); - })); + const container = new Container(); + container.bind("Constructable").to(Constructable).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + resolved = true; + r(c); + })); - const result = await container.getAsync("Constructable"); + const result = await container.getAsync("Constructable"); - expect(result).instanceof(Constructable); - expect(resolved).eql(true); - }); + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); - it("Should wait until postConstruct promise resolves before returning object", async () => { - let resolved = false; + it("Should wait until postConstruct promise resolves before returning object", async () => { + let resolved = false; - @injectable() - class Constructable { - @postConstruct() - public myPostConstructMethod() { - return new Promise((r) => { - resolved = true; - r({}); - }); - } - } + @injectable() + class Constructable { + @postConstruct() + public myPostConstructMethod() { + return new Promise((r) => { + resolved = true; + r({}); + }); + } + } - const container = new Container(); - container.bind("Constructable").to(Constructable); + const container = new Container(); + container.bind("Constructable").to(Constructable); - const result = await container.getAsync("Constructable"); + const result = await container.getAsync("Constructable"); - expect(result).instanceof(Constructable); - expect(resolved).eql(true); - }); + expect(result).instanceof(Constructable); + expect(resolved).eql(true); + }); - it("Should only call async method once if marked as singleton (indirect)", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const subject1 = await container.getAsync("UseDate"); - const subject2 = await container.getAsync("UseDate"); - expect(subject1.doSomething() === subject2.doSomething()).eql(true); - }); - - it("Should support async singletons when using autoBindInjectable", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public date: Date; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { - expect(asyncValue).instanceOf(AsyncValue); - - this.asyncValue = asyncValue; - } - } - - const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const object1 = await container.getAsync(MixedDependency); - const object2 = await container.getAsync(MixedDependency); - - expect(object1).equals(object2); - }); - - it("Should support shared async singletons when using autoBindInjectable", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { - expect(asyncValue).instanceOf(AsyncValue); - - this.asyncValue = asyncValue; - } - } - - const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const async = await container.getAsync(AsyncValue); - - const object1 = await container.getAsync(MixedDependency); - - expect(async).equals(object1.asyncValue); - }); - - it("Should support async dependencies in multiple layers", async () => { - @injectable() - class AsyncValue { - public date: Date; - public constructor(@inject("Date") date: Date) { - //expect(date).instanceOf(date); - - this.date = date; - } - } - - @injectable() - class MixedDependency { - public asyncValue: AsyncValue; - public date: Date; - public constructor(@inject(AsyncValue) asyncValue: AsyncValue, @inject("Date") date: Date) { - expect(asyncValue).instanceOf(AsyncValue); - expect(date).instanceOf(Date); - - this.date = date; - this.asyncValue = asyncValue; - } - } - - const container = new Container({ autoBindInjectable: true }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - const subject1 = await container.getAsync(MixedDependency); - expect(subject1.date).instanceOf(Date); - expect(subject1.asyncValue).instanceOf(AsyncValue); - }); - - it("Should support async values already in cache", async () => { - const container = new Container({ autoBindInjectable: true }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - expect(await container.getAsync("Date")).instanceOf(Date); - }); - - it("Should support async values already in cache when there dependencies", async () => { - @injectable() - class HasDependencies { - public constructor(@inject("Date") date: Date) { - expect(date).instanceOf(Date); - } - } - - const container = new Container({ autoBindInjectable: true }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - await container.getAsync(HasDependencies); - }); - - it("Should support async values already in cache when there are transient dependencies", async () => { - @injectable() - class Parent { - public constructor(@inject("Date") date: Date) { - expect(date).instanceOf(Date); - } - } - - @injectable() - class Child { - public constructor( - @inject(Parent) parent: Parent, - @inject("Date") date: Date - ) { - expect(parent).instanceOf(Parent); - expect(date).instanceOf(Date); - } - } - - const container = new Container({ autoBindInjectable: true }); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - - expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object - await container.getAsync(Child); - }); - - it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public foobar: string; - - public constructor(@inject("Date") currentDate: Date, @inject("Static") foobar: string) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - this.foobar = foobar; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - container.bind("Static").toConstantValue("foobar"); - - const subject1 = await container.getAsync("UseDate"); - expect(subject1.foobar).eql("foobar"); - }); - - it("Should throw exception if using sync API with async dependencies", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - - expect(() => container.get("UseDate")).to.throw(`You are attempting to construct 'UseDate' in a synchronous way + it("Should only call async method once if marked as singleton (indirect)", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + expect(subject1.doSomething() === subject2.doSomething()).eql(true); + }); + + it("Should support async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); + + this.asyncValue = asyncValue; + } + } + + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const object1 = await container.getAsync(MixedDependency); + const object2 = await container.getAsync(MixedDependency); + + expect(object1).equals(object2); + }); + + it("Should support shared async singletons when using autoBindInjectable", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue) { + expect(asyncValue).instanceOf(AsyncValue); + + this.asyncValue = asyncValue; + } + } + + const container = new Container({ autoBindInjectable: true, defaultScope: "Singleton" }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const async = await container.getAsync(AsyncValue); + + const object1 = await container.getAsync(MixedDependency); + + expect(async).equals(object1.asyncValue); + }); + + it("Should support async dependencies in multiple layers", async () => { + @injectable() + class AsyncValue { + public date: Date; + public constructor(@inject("Date") date: Date) { + //expect(date).instanceOf(date); + + this.date = date; + } + } + + @injectable() + class MixedDependency { + public asyncValue: AsyncValue; + public date: Date; + public constructor(@inject(AsyncValue) asyncValue: AsyncValue, @inject("Date") date: Date) { + expect(asyncValue).instanceOf(AsyncValue); + expect(date).instanceOf(Date); + + this.date = date; + this.asyncValue = asyncValue; + } + } + + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + const subject1 = await container.getAsync(MixedDependency); + expect(subject1.date).instanceOf(Date); + expect(subject1.asyncValue).instanceOf(AsyncValue); + }); + + it("Should support async values already in cache", async () => { + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + expect(await container.getAsync("Date")).instanceOf(Date); + }); + + it("Should support async values already in cache when there dependencies", async () => { + @injectable() + class HasDependencies { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } + + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(HasDependencies); + }); + + it("Should support async values already in cache when there are transient dependencies", async () => { + @injectable() + class Parent { + public constructor(@inject("Date") date: Date) { + expect(date).instanceOf(Date); + } + } + + @injectable() + class Child { + public constructor( + @inject(Parent) parent: Parent, + @inject("Date") date: Date + ) { + expect(parent).instanceOf(Parent); + expect(date).instanceOf(Date); + } + } + + const container = new Container({ autoBindInjectable: true }); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); + + expect(await container.getAsync("Date")).instanceOf(Date); // causes container to cache singleton as Lazy object + await container.getAsync(Child); + }); + + it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public foobar: string; + + public constructor(@inject("Date") currentDate: Date, @inject("Static") foobar: string) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + this.foobar = foobar; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + container.bind("Static").toConstantValue("foobar"); + + const subject1 = await container.getAsync("UseDate"); + expect(subject1.foobar).eql("foobar"); + }); + + it("Should throw exception if using sync API with async dependencies", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + + expect(() => container.get("UseDate")).to.throw(`You are attempting to construct 'UseDate' in a synchronous way but it has asynchronous dependencies.`); - }); - - it("Should be able to resolve indirect Promise bindings", async () => { - @injectable() - class UseDate implements UseDate { - public currentDate: Date; - public constructor(@inject("Date") currentDate: Date) { - expect(currentDate).instanceOf(Date); - - this.currentDate = currentDate; - } - public doSomething() { - return this.currentDate; - } - } - - const container = new Container(); - container.bind("UseDate").to(UseDate); - container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); - - const subject1 = await container.getAsync("UseDate"); - const subject2 = await container.getAsync("UseDate"); - // tslint:disable-next-line:no-console - console.log(subject1, subject2); - expect(subject1.doSomething() === subject2.doSomething()).eql(false); - }); - - it("Should be able to resolve direct promise bindings", async () => { - const container = new Container(); - container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - - const value = await container.getAsync("async"); - expect(value).eql("foobar"); - }); - - it("Should error if trying to resolve an promise in sync API", () => { - const container = new Container(); - container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); - - expect(() => container.get("async")).to.throw(`You are attempting to construct 'async' in a synchronous way + }); + + it("Should be able to resolve indirect Promise bindings", async () => { + @injectable() + class UseDate implements UseDate { + public currentDate: Date; + public constructor(@inject("Date") currentDate: Date) { + expect(currentDate).instanceOf(Date); + + this.currentDate = currentDate; + } + public doSomething() { + return this.currentDate; + } + } + + const container = new Container(); + container.bind("UseDate").to(UseDate); + container.bind("Date").toDynamicValue(() => Promise.resolve(new Date())); + + const subject1 = await container.getAsync("UseDate"); + const subject2 = await container.getAsync("UseDate"); + // tslint:disable-next-line:no-console + console.log(subject1, subject2); + expect(subject1.doSomething() === subject2.doSomething()).eql(false); + }); + + it("Should be able to resolve direct promise bindings", async () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); + + const value = await container.getAsync("async"); + expect(value).eql("foobar"); + }); + + it("Should error if trying to resolve an promise in sync API", () => { + const container = new Container(); + container.bind("async").toDynamicValue(() => Promise.resolve("foobar")); + + expect(() => container.get("async")).to.throw(`You are attempting to construct 'async' in a synchronous way but it has asynchronous dependencies.`); - }); + }); }); From 01941fd0af7a89ca03b7cedefa44ae69a6e1a9ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Mar 2021 09:27:18 -0500 Subject: [PATCH 18/64] Bump elliptic from 6.5.3 to 6.5.4 (#1298) Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4. - [Release notes](https://github.com/indutny/elliptic/releases) - [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index e243da4ce..860951558 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1905,24 +1905,30 @@ } }, "elliptic": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", - "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", "dev": true, "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", + "bn.js": "^4.11.9", + "brorand": "^1.1.0", "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" }, "dependencies": { "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true } } From 525007b073cf803c9892102d4f93065b2a695764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Mon, 12 Apr 2021 11:13:02 +0200 Subject: [PATCH 19/64] constant value / function binding activated when resolved (#1122) --- src/resolution/resolver.ts | 2 ++ test/resolution/resolver.test.ts | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index e659884c2..dbc2f137f 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -76,8 +76,10 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => if (binding.type === BindingTypeEnum.ConstantValue) { result = binding.cache; + binding.activated = true; } else if (binding.type === BindingTypeEnum.Function) { result = binding.cache; + binding.activated = true; } else if (binding.type === BindingTypeEnum.Constructor) { result = binding.implementationType; } else if (binding.type === BindingTypeEnum.DynamicValue && binding.dynamicValue !== null) { diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index c70be3147..5029db138 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -176,14 +176,18 @@ describe("Resolve", () => { const bindingDictionary = getBindingDictionary(container); const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - expect(bindingDictionary.get(katanaId)[0].cache === null).eql(true); + const katanaBinding = bindingDictionary.get(katanaId)[0]; + expect(katanaBinding.cache === null).eql(true); + expect(katanaBinding.activated).eql(false); + const ninja = resolve(context); expect(ninja instanceof Ninja).eql(true); const ninja2 = resolve(context); expect(ninja2 instanceof Ninja).eql(true); - expect(bindingDictionary.get(katanaId)[0].cache instanceof Katana).eql(true); + expect(katanaBinding.cache instanceof Katana).eql(true); + expect(katanaBinding.activated).eql(true); }); @@ -295,8 +299,13 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + const katanaBinding = getBindingDictionary(container).get(katanaId)[0]; + expect(katanaBinding.activated).eql(false); + const ninja = resolve(context); + expect(katanaBinding.activated).eql(true); + expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); expect(ninja.katana.handler instanceof KatanaHandler).eql(true); @@ -1004,6 +1013,9 @@ describe("Resolve", () => { container.bind(katanaFactoryId).toFunction(katanaFactoryInstance); + const katanaFactoryBinding = getBindingDictionary(container).get(katanaFactoryId)[0]; + expect(katanaFactoryBinding.activated).eql(false); + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); const ninja = resolve(context); @@ -1014,8 +1026,9 @@ describe("Resolve", () => { expect(ninja.katanaFactory().handler instanceof KatanaHandler).eql(true); expect(ninja.katanaFactory().blade instanceof KatanaBlade).eql(true); expect(ninja.shuriken instanceof Shuriken).eql(true); + expect(katanaFactoryBinding.activated).eql(true); - }); + }); it("Should run the @PostConstruct method", () => { From a67b3a7043af717bb63263feb504efda27e8c194 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Tue, 30 Mar 2021 19:57:02 +0100 Subject: [PATCH 20/64] Apply middleware when Container.resolve - fixes 1128 (#1129) * middleware applied --- src/container/container.ts | 6 ++ test/features/resolve_unbinded.test.ts | 90 +++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/container/container.ts b/src/container/container.ts index d86951e38..0b0b4d29a 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -24,6 +24,7 @@ class Container implements interfaces.Container { private _deactivations: interfaces.Lookup>; private _snapshots: interfaces.ContainerSnapshot[]; private _metadataReader: interfaces.MetadataReader; + private _appliedMiddleware: interfaces.Middleware[] = []; public static merge( container1: interfaces.Container, @@ -307,6 +308,7 @@ class Container implements interfaces.Container { } public applyMiddleware(...middlewares: interfaces.Middleware[]): void { + this._appliedMiddleware = this._appliedMiddleware.concat(middlewares); const initial: interfaces.Next = (this._middleware) ? this._middleware : this._planAndResolve(); this._middleware = middlewares.reduce( (prev, curr) => curr(prev), @@ -377,6 +379,10 @@ class Container implements interfaces.Container { public resolve(constructorFunction: interfaces.Newable) { const tempContainer = this.createChild(); tempContainer.bind(constructorFunction).toSelf(); + this._appliedMiddleware.forEach((m) => { + tempContainer.applyMiddleware(m); + }); + return tempContainer.get(constructorFunction); } diff --git a/test/features/resolve_unbinded.test.ts b/test/features/resolve_unbinded.test.ts index 318f61aaa..fd14b8bbc 100644 --- a/test/features/resolve_unbinded.test.ts +++ b/test/features/resolve_unbinded.test.ts @@ -1,5 +1,5 @@ import { expect } from "chai"; -import { Container, injectable } from "../../src/inversify"; +import { Container, injectable, interfaces } from "../../src/inversify"; describe("Container.prototype.resolve", () => { @@ -31,5 +31,93 @@ describe("Container.prototype.resolve", () => { expect(ninja.fight()).to.eql("cut!"); }); + describe("Should use middleware", () => { + interface TestMiddlewareAppliedInCorrectOrder { + description: string; + applyMiddleware: ( + container: interfaces.Container, + middleware1: interfaces.Middleware, + middleware2: interfaces.Middleware, + middleware3: interfaces.Middleware, + middleware4: interfaces.Middleware) => void; + } + const middlewareOrderTests: TestMiddlewareAppliedInCorrectOrder[] = [ + { + applyMiddleware: (container, middleware1, middleware2, middleware3, middleware4) => { + container.applyMiddleware(middleware1, middleware2, middleware3, middleware4); + }, + description: "All at once", + }, + { + applyMiddleware: (container, middleware1, middleware2, middleware3, middleware4) => { + container.applyMiddleware(middleware1, middleware2); + container.applyMiddleware(middleware3, middleware4); + }, + description: "Two calls", + } + ]; + middlewareOrderTests.forEach((t) => testApplyMiddlewareSameOrder(t)); + function testApplyMiddlewareSameOrder(test: TestMiddlewareAppliedInCorrectOrder) { + it(test.description, () => { + @injectable() + class Katana { + public hit() { + return "cut!"; + } + } + + @injectable() + class Ninja implements Ninja { + public katana: Katana; + public constructor(katana: Katana) { + this.katana = katana; + } + public fight() { return this.katana.hit(); } + } + const middlewareOrder: number[] = []; + const middleware1: interfaces.Middleware = (next) => { + return (args) => { + middlewareOrder.push(1); + return next(args); + }; + }; + const middleware2: interfaces.Middleware = (next) => { + return (args) => { + middlewareOrder.push(2); + return next(args); + }; + }; + const middleware3: interfaces.Middleware = (next) => { + return (args) => { + middlewareOrder.push(3); + return next(args); + }; + }; + const middleware4: interfaces.Middleware = (next) => { + return (args) => { + middlewareOrder.push(4); + return next(args); + }; + }; + const resolveContainer = new Container(); + resolveContainer.bind(Katana).toSelf(); + test.applyMiddleware(resolveContainer, middleware1, middleware2, middleware3, middleware4); + + const getContainer = new Container(); + getContainer.bind(Katana).toSelf(); + getContainer.bind(Ninja).toSelf(); + test.applyMiddleware(getContainer, middleware1, middleware2, middleware3, middleware4); + + resolveContainer.resolve(Ninja); + getContainer.get(Ninja); + + expect(middlewareOrder.length).eql(8); + expect(middlewareOrder[0]).eql(middlewareOrder[4]); + expect(middlewareOrder[1]).eql(middlewareOrder[5]); + expect(middlewareOrder[2]).eql(middlewareOrder[6]); + expect(middlewareOrder[3]).eql(middlewareOrder[7]); + }); + } + }); }); From c1e737634bd14c0773a248de8060b98234f1bc57 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Tue, 30 Mar 2021 20:08:19 +0100 Subject: [PATCH 21/64] Setter injection without cast (#1152) * setter injection without cast --- src/annotation/inject.ts | 2 +- test/inversify.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/annotation/inject.ts b/src/annotation/inject.ts index 0e17ba209..9e67fa30a 100644 --- a/src/annotation/inject.ts +++ b/src/annotation/inject.ts @@ -18,7 +18,7 @@ export class LazyServiceIdentifer { } function inject(serviceIdentifier: ServiceIdentifierOrFunc) { - return function(target: any, targetKey: string, index?: number): void { + return function(target: any, targetKey: string, index?: number | PropertyDescriptor): void { if (serviceIdentifier === undefined) { throw new Error(UNDEFINED_INJECT_ANNOTATION(target.name)); } diff --git a/test/inversify.test.ts b/test/inversify.test.ts index 3f0314e33..3d30ac058 100644 --- a/test/inversify.test.ts +++ b/test/inversify.test.ts @@ -72,6 +72,44 @@ describe("InversifyJS", () => { }); + it("Should be able to to do setter injection and property injection", () => { + @injectable() + class Shuriken { + public throw() { + return "hit!"; + } + } + @injectable() + class Katana implements Katana { + public hit() { + return "cut!"; + } + } + + @injectable() + class Ninja { + + private _shuriken: Shuriken; + @inject(Shuriken) + public set Shuriken(shuriken: Shuriken) { + this._shuriken = shuriken; + } + @inject(Katana) + public katana!: Katana; + public sneak() { return this._shuriken.throw(); } + public fight() {return this.katana.hit(); } + + } + + const container = new Container(); + container.bind("Ninja").to(Ninja); + container.bind(Shuriken).toSelf(); + container.bind(Katana).toSelf(); + + const ninja = container.get("Ninja"); + expect(ninja.sneak()).to.eql("hit!"); + expect(ninja.fight()).to.eql("cut!"); + }); it("Should be able to resolve and inject dependencies in VanillaJS", () => { const TYPES = { From 80e91bd0c5a5b4d81c61d4e390fc77787146f90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Thu, 8 Apr 2021 13:26:24 +0200 Subject: [PATCH 22/64] feat: refactor Container extracting logic to _preDestroyBinding and _removeServiceFromDictionary --- src/container/container.ts | 98 ++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 0b0b4d29a..98172453d 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -176,64 +176,36 @@ class Container implements interfaces.Container { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - for (const binding of bindings) { - const result = this.preDestroy(binding); - - if (isPromise(result)) { - throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); - } - } + this._preDestroyBindings(bindings); } - try { - this._bindingDictionary.remove(serviceIdentifier); - } catch (e) { - throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); - } + this._removeServiceFromDictionary(serviceIdentifier); } public async unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - for (const binding of bindings) { - await this.preDestroy(binding); - } + await this._preDestroyBindingsAsync(bindings); } - try { - this._bindingDictionary.remove(serviceIdentifier); - } catch (e) { - throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); - } + this._removeServiceFromDictionary(serviceIdentifier); } // Removes all the type bindings from the registry public unbindAll(): void { this._bindingDictionary.traverse((key, value) => { - for (const binding of value) { - const result = this.preDestroy(binding); - - if (isPromise(result)) { - throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); - } - } + this._preDestroyBindings(value); }); this._bindingDictionary = new Lookup>(); } public async unbindAllAsync(): Promise { - const promises: Promise[] = []; + const promises: Promise[] = []; this._bindingDictionary.traverse((key, value) => { - for (const binding of value) { - const result = this.preDestroy(binding); - - if (isPromise(result)) { - promises.push(result); - } - } + promises.push(this._preDestroyBindingsAsync(value)); }); await Promise.all(promises); @@ -386,18 +358,6 @@ class Container implements interfaces.Container { return tempContainer.get(constructorFunction); } - private preDestroy(binding: Binding): Promise | void { - if (!binding.cache) { - return; - } - - if (isPromise(binding.cache)) { - return binding.cache.then((resolved: any) => this.doDeactivation(binding, resolved)); - } - - return this.doDeactivation(binding, binding.cache); - } - private doDeactivation( binding: Binding, instance: T, @@ -581,6 +541,50 @@ class Container implements interfaces.Container { }; } + private _preDestroyBinding(binding: Binding): Promise | void { + if (!binding.cache) { + return; + } + + if (isPromise(binding.cache)) { + return binding.cache.then((resolved: any) => this.doDeactivation(binding, resolved)); + } + + return this.doDeactivation(binding, binding.cache); + } + + private _preDestroyBindings(bindings: Binding[]): void { + for (const binding of bindings) { + const result = this._preDestroyBinding(binding); + + if (isPromise(result)) { + throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); + } + } + } + + private async _preDestroyBindingsAsync(bindings: Binding[]): Promise { + const promises: Promise[] = []; + + for (const binding of bindings) { + const result = this._preDestroyBinding(binding); + + if (isPromise(result)) { + promises.push(result); + } + } + + await Promise.all(promises); + } + + private _removeServiceFromDictionary(serviceIdentifier: interfaces.ServiceIdentifier): void { + try { + this._bindingDictionary.remove(serviceIdentifier); + } catch (e) { + throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); + } + } + } export { Container }; From 8e77c5117fdf429beaf7865e8c3139a20879da0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Thu, 8 Apr 2021 13:31:29 +0200 Subject: [PATCH 23/64] refactor: updated several modules to pass codeclimate's analysis. This is a squash commit. It includes the following changes: style: remove trailing whitespace test: add async deactivation test case feat: update container.doDeactivation to use a simpler iterableiterator test: update test to be async refactor: extract common decorator logic to propertyEventDecorator refactor: update Container._doDeactivation method and calls to remove redundant error handling logic refactor: update Container._doDeactivation to get instance constructor in the same way as in the rest of the project style: update tsConfig with missing line feed refactor: update _doServiceBindingsDeactivation to not to receive a binding refactor: update activationLoop to receive an iterable iterator of activation bindings refactor: update _onActivation with less paramenters and loc. refactor: update onActivation extracting container iterator generation in a _onActivationGetContainersToTraverse method refactor: update _onActivation to avoid recursion. An iterative strategy has been used instead of a recursive one. refactor:_activationLoop no longer needs more than 4 parameters and it's logic has been extracted to several methods update onActivation to reduce cognitive complexity refactor: update resolveInstance to reduce its cognitive complexity refactor: extract common logic to _filterRequestsByTargetType method refactor: refactor _get to reduce paramenters and split async handling in a _getWithAsyncContext method refactor: express AsyncContainerModuleCallBack as an AsyncCallback of ContainerModuleCallBack style: add missing line feed --- src/annotation/post_construct.ts | 16 +- src/annotation/pre_destroy.ts | 16 +- src/annotation/property_event_decorator.ts | 16 ++ src/container/container.ts | 263 ++++++++++++++------- src/interfaces/interfaces.ts | 13 +- src/resolution/instantiation.ts | 137 +++++++---- src/resolution/resolver.ts | 168 +++++++++---- test/container/container_module.test.ts | 19 ++ test/resolution/resolver.test.ts | 18 +- tsconfig.json | 2 +- 10 files changed, 461 insertions(+), 207 deletions(-) create mode 100644 src/annotation/property_event_decorator.ts diff --git a/src/annotation/post_construct.ts b/src/annotation/post_construct.ts index 355bb32a8..4116cee8a 100644 --- a/src/annotation/post_construct.ts +++ b/src/annotation/post_construct.ts @@ -1,16 +1,10 @@ import * as ERRORS_MSGS from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; -import { Metadata } from "../planning/metadata"; +import { propertyEventDecorator } from "./property_event_decorator"; -function postConstruct() { - return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { - const metadata = new Metadata(METADATA_KEY.POST_CONSTRUCT, propertyKey); - - if (Reflect.hasOwnMetadata(METADATA_KEY.POST_CONSTRUCT, target.constructor)) { - throw new Error(ERRORS_MSGS.MULTIPLE_POST_CONSTRUCT_METHODS); - } - Reflect.defineMetadata(METADATA_KEY.POST_CONSTRUCT, metadata, target.constructor); - }; -} +const postConstruct = propertyEventDecorator( + METADATA_KEY.POST_CONSTRUCT, + ERRORS_MSGS.MULTIPLE_POST_CONSTRUCT_METHODS, +); export { postConstruct }; diff --git a/src/annotation/pre_destroy.ts b/src/annotation/pre_destroy.ts index 9dd3d3841..97887ed80 100644 --- a/src/annotation/pre_destroy.ts +++ b/src/annotation/pre_destroy.ts @@ -1,16 +1,10 @@ import * as ERRORS_MSGS from "../constants/error_msgs"; import * as METADATA_KEY from "../constants/metadata_keys"; -import { Metadata } from "../planning/metadata"; +import { propertyEventDecorator } from "./property_event_decorator"; -function preDestroy() { - return function (target: any, propertyKey: string) { - const metadata = new Metadata(METADATA_KEY.PRE_DESTROY, propertyKey); - - if (Reflect.hasOwnMetadata(METADATA_KEY.PRE_DESTROY, target.constructor)) { - throw new Error(ERRORS_MSGS.MULTIPLE_PRE_DESTROY_METHODS); - } - Reflect.defineMetadata(METADATA_KEY.PRE_DESTROY, metadata, target.constructor); - }; -} +const preDestroy = propertyEventDecorator( + METADATA_KEY.PRE_DESTROY, + ERRORS_MSGS.MULTIPLE_PRE_DESTROY_METHODS, +); export { preDestroy }; diff --git a/src/annotation/property_event_decorator.ts b/src/annotation/property_event_decorator.ts new file mode 100644 index 000000000..1cdec551c --- /dev/null +++ b/src/annotation/property_event_decorator.ts @@ -0,0 +1,16 @@ +import { Metadata } from "../planning/metadata"; + +function propertyEventDecorator(eventKey: string, errorMessage: string) { + return () => { + return (target: any, propertyKey: string) => { + const metadata = new Metadata(eventKey, propertyKey); + + if (Reflect.hasOwnMetadata(eventKey, target.constructor)) { + throw new Error(errorMessage); + } + Reflect.defineMetadata(eventKey, metadata, target.constructor); + } + } +} + +export { propertyEventDecorator } diff --git a/src/container/container.ts b/src/container/container.ts index 98172453d..7fbfe5d3b 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -295,19 +295,27 @@ class Container implements interfaces.Container { // The runtime identifier must be associated with only one binding // use getAll when the runtime identifier is associated with multiple bindings public get(serviceIdentifier: interfaces.ServiceIdentifier): T { - return this._get(false, false, false, TargetTypeEnum.Variable, serviceIdentifier) as T; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, false); + + return this._getWithAsyncContext(false, defaultArgs) as T; } public getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { - return this._get(true, false, false, TargetTypeEnum.Variable, serviceIdentifier) as Promise; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, false); + + return this._getWithAsyncContext(true, defaultArgs) as Promise; } public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T { - return this._get(false, false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, false, key, value); + + return this._getWithAsyncContext(false, defaultArgs) as T; } public getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise { - return this._get(true, false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as Promise; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, false, key, value); + + return this._getWithAsyncContext(true, defaultArgs) as Promise; } public getNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T { @@ -321,15 +329,21 @@ class Container implements interfaces.Container { // Resolves a dependency by its runtime identifier // The runtime identifier can be associated with one or multiple bindings public getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] { - return this._get(false, true, true, TargetTypeEnum.Variable, serviceIdentifier) as T[]; + const defaultArgs = this._getAllDefaultArgs(serviceIdentifier); + + return this._getWithAsyncContext(false, defaultArgs) as T[]; } public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise[] { - return this._get(true, true, true, TargetTypeEnum.Variable, serviceIdentifier) as Promise[]; + const defaultArgs = this._getAllDefaultArgs(serviceIdentifier); + + return this._getWithAsyncContext(true, defaultArgs) as Promise[]; } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { - return this._get(false, false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T[]; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, true, key, value); + + return this._getWithAsyncContext(false, defaultArgs) as T[]; } public getAllTaggedAsync( @@ -337,7 +351,9 @@ class Container implements interfaces.Container { key: string | number | symbol, value: any ): Promise[] { - return this._get(true, false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as Promise[]; + const defaultArgs = this._getDefaultArgs(serviceIdentifier, true, key, value); + + return this._getWithAsyncContext(true, defaultArgs) as Promise[]; } public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { @@ -358,74 +374,86 @@ class Container implements interfaces.Container { return tempContainer.get(constructorFunction); } - private doDeactivation( - binding: Binding, - instance: T, - iter?: IterableIterator<[number, interfaces.BindingDeactivation]> - ): void | Promise { - let constr: any; + private _destroyMetadata(constructor: any, instance: any) { + if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constructor)) { + const data: interfaces.Metadata = Reflect.getMetadata(METADATA_KEY.PRE_DESTROY, constructor); - try { - constr = (instance as any).constructor; - } catch (ex) { - // if placing mocks in container (eg: TypeMoq), this could blow up as constructor is not stubbed - return; + return instance[data.value](); } + } + + private _doDeactivation(binding: Binding, instance: T): void | Promise { + const constructor = Object.getPrototypeOf(instance).constructor; try { if (this._deactivations.hasKey(binding.serviceIdentifier)) { - const deactivations = iter || this._deactivations.get(binding.serviceIdentifier).entries(); - - let deact = deactivations.next(); + const result = this._doServiceBindingsDeactivation( + instance, + this._deactivations.get(binding.serviceIdentifier).values(), + ); - while (deact.value) { - const result = deact.value[1](instance); + if (isPromise(result)) { + return this._handleDeactivationError( + result.then(() => this._propagateDeactivationAsync(binding, instance, constructor)), + constructor + ); + } + } - if (isPromise(result)) { - return result.then(() => { - this.doDeactivation(binding, instance, deactivations); - }).catch((ex) => { - throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); - }); - } + const propagateDeactivationResult = this._propagateDeactivation(binding, instance, constructor); - deact = deactivations.next(); - } + if (isPromise(propagateDeactivationResult)) { + return this._handleDeactivationError(propagateDeactivationResult, constructor); } } catch (ex) { - throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constructor.name, ex.message)); } + } - if (this.parent) { - return this.doDeactivation.bind(this.parent)(binding, instance); + private async _handleDeactivationError(asyncResult: Promise, constructor: any): Promise { + try { + await asyncResult + } catch (ex) { + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constructor.name, ex.message)); } + } - try { - if (typeof binding.onDeactivation === "function") { - const result = binding.onDeactivation(instance); - if (isPromise(result)) { - return result.then(() => this.destroyMetadata(constr, instance)); - } + private _doServiceBindingsDeactivation( + instance: T, + deactivationsIterator: IterableIterator>, + ): void | Promise { + let deactivation = deactivationsIterator.next(); + + while (deactivation.value) { + const result = deactivation.value(instance); + + if (isPromise(result)) { + return result.then(() => + this._doServicesBindingsDeactivationAsync(instance, deactivationsIterator), + ); } - return this.destroyMetadata(constr, instance); - } catch (ex) { - throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constr.name, ex.message)); + deactivation = deactivationsIterator.next(); } } - private destroyMetadata(constr: any, instance: any) { - if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constr)) { - const data: interfaces.Metadata = Reflect.getMetadata(METADATA_KEY.PRE_DESTROY, constr); - try { - return instance[data.value](); - } catch (e) { - throw new Error(ERROR_MSGS.PRE_DESTROY_ERROR(constr.name, e.message)); - } + private async _doServicesBindingsDeactivationAsync( + instance: T, + deactivationsIterator: IterableIterator>, + ): Promise { + let deactivation = deactivationsIterator.next(); + + while (deactivation.value) { + await deactivation.value(instance); + deactivation = deactivationsIterator.next(); } } + private _getDefaultContextInterceptor(): (context: interfaces.Context) => interfaces.Context { + return (context) => context; + } + private _getContainerModuleHelpersFactory() { const setModuleId = (bindingToSyntax: any, moduleId: number) => { @@ -474,27 +502,8 @@ class Container implements interfaces.Container { // Prepares arguments required for resolution and // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve - private _get( - async: boolean, - avoidConstraints: boolean, - isMultiInject: boolean, - targetType: interfaces.TargetType, - serviceIdentifier: interfaces.ServiceIdentifier, - key?: string | number | symbol, - value?: any - ): (T | T[] | Promise | Promise[]) { - - let result: (T | T[]) | null = null; - - const defaultArgs: interfaces.NextArgs = { - avoidConstraints, - contextInterceptor: (context: interfaces.Context) => context, - isMultiInject, - key, - serviceIdentifier, - targetType, - value - }; + private _get(defaultArgs: interfaces.NextArgs): (T | T[] | Promise | Promise[]) { + let result: (T | T[] | Promise | Promise[]) | null = null; if (this._middleware) { result = this._middleware(defaultArgs); @@ -505,13 +514,53 @@ class Container implements interfaces.Container { result = this._planAndResolve()(defaultArgs); } - if (isPromise(result) && !async) { - throw new Error(ERROR_MSGS.LAZY_IN_SYNC(serviceIdentifier)); + return result; + } + + private _getWithAsyncContext( + isAsync: boolean, + defaultArgs: interfaces.NextArgs, + ): (T | T[] | Promise | Promise[]) { + const result = this._get(defaultArgs); + + if (isPromise(result) && !isAsync) { + throw new Error(ERROR_MSGS.LAZY_IN_SYNC(defaultArgs.serviceIdentifier)); } return result; } + private _getAllDefaultArgs(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.NextArgs { + const defaultArgs: interfaces.NextArgs = { + avoidConstraints: true, + contextInterceptor: this._getDefaultContextInterceptor(), + isMultiInject: true, + targetType: TargetTypeEnum.Variable, + serviceIdentifier, + }; + + return defaultArgs; + } + + private _getDefaultArgs( + serviceIdentifier: interfaces.ServiceIdentifier, + isMultiInject: boolean, + key?: string | number | symbol, + value?: any, + ): interfaces.NextArgs { + const defaultArgs: interfaces.NextArgs = { + avoidConstraints: false, + contextInterceptor: this._getDefaultContextInterceptor(), + isMultiInject, + targetType: TargetTypeEnum.Variable, + serviceIdentifier, + key, + value, + }; + + return defaultArgs; + } + // Planner creates a plan and Resolver resolves a plan // one of the jobs of the Container is to links the Planner // with the Resolver and that is what this function is about @@ -547,10 +596,10 @@ class Container implements interfaces.Container { } if (isPromise(binding.cache)) { - return binding.cache.then((resolved: any) => this.doDeactivation(binding, resolved)); + return binding.cache.then((resolved: any) => this._doDeactivation(binding, resolved)); } - return this.doDeactivation(binding, binding.cache); + return this._doDeactivation(binding, binding.cache); } private _preDestroyBindings(bindings: Binding[]): void { @@ -565,7 +614,7 @@ class Container implements interfaces.Container { private async _preDestroyBindingsAsync(bindings: Binding[]): Promise { const promises: Promise[] = []; - + for (const binding of bindings) { const result = this._preDestroyBinding(binding); @@ -577,6 +626,34 @@ class Container implements interfaces.Container { await Promise.all(promises); } + private _propagateDeactivation( + binding: Binding, + instance: T, + constructor: any + ): void | Promise { + if (this.parent) { + const parentDeactivationResult = this._doDeactivation.bind(this.parent)(binding, instance); + + if (isPromise(parentDeactivationResult)) { + return this._triggerOnDeactivationAndDestroyMetadataAsync(binding, instance, constructor); + } + } else { + return this._triggerOnDeactivationAndDestroyMetadata(binding, instance, constructor); + } + } + + private async _propagateDeactivationAsync( + binding: Binding, + instance: T, + constructor: any + ): Promise { + if (this.parent) { + await this._doDeactivation.bind(this.parent)(binding, instance); + } else { + await this._triggerOnDeactivationAndDestroyMetadataAsync(binding, instance, constructor); + } + } + private _removeServiceFromDictionary(serviceIdentifier: interfaces.ServiceIdentifier): void { try { this._bindingDictionary.remove(serviceIdentifier); @@ -585,6 +662,34 @@ class Container implements interfaces.Container { } } + private _triggerOnDeactivationAndDestroyMetadata( + binding: Binding, + instance: T, + constructor: any + ): void | Promise { + if (typeof binding.onDeactivation === "function") { + const result = binding.onDeactivation(instance); + + if (isPromise(result)) { + return result.then(() => this._destroyMetadata(constructor, instance)); + } + } + + return this._destroyMetadata(constructor, instance); + } + + private async _triggerOnDeactivationAndDestroyMetadataAsync( + binding: Binding, + instance: T, + constructor: any + ): Promise { + if (typeof binding.onDeactivation === "function") { + await binding.onDeactivation(instance); + } + + await this._destroyMetadata(constructor, instance); + } + } export { Container }; diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index d3500135c..0dfcaf393 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -1,5 +1,9 @@ namespace interfaces { + type AsyncCallback = + TCallback extends (...args: infer TArgs) => infer TResult ? (...args: TArgs) => Promise + : never; + export type BindingScope = "Singleton" | "Transient" | "Request"; export type BindingType = "ConstantValue" | "Constructor" | "DynamicValue" | "Factory" | @@ -227,14 +231,7 @@ namespace interfaces { onDeactivation: interfaces.Container["onDeactivation"] ) => void; - export type AsyncContainerModuleCallBack = ( - bind: interfaces.Bind, - unbind: interfaces.Unbind, - isBound: interfaces.IsBound, - rebind: interfaces.Rebind, - onActivation: interfaces.Container["onActivation"], - onDeactivation: interfaces.Container["onDeactivation"] - ) => Promise; + export type AsyncContainerModuleCallBack = AsyncCallback; export interface ContainerSnapshot { bindings: Lookup>; diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 18f460324..e644358c9 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -10,11 +10,7 @@ function _injectProperties( childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler ): any { - const propertyInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => - ( - childRequest.target !== null && - childRequest.target.type === TargetTypeEnum.ClassProperty - )); + const propertyInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ClassProperty); const propertyInjections = propertyInjectionsRequests.map(resolveRequest); @@ -29,8 +25,81 @@ function _injectProperties( } -function _createInstance(Func: interfaces.Newable, injections: Object[]): any { - return new Func(...injections); +function _createInstance( + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): any { + let result: any; + + if (childRequests.length > 0) { + const constructorInjections = _getConstructionInjections(childRequests, resolveRequest); + + if (constructorInjections.some(isPromise)) { + result = _createInstanceWithConstructorInjectionsAsync( + constructorInjections, + constr, + childRequests, + resolveRequest + ); + } else { + result = _createInstanceWithConstructorInjections(constructorInjections, constr,childRequests, resolveRequest); + } + } else { + result = new constr(); + } + + return result; +} + +function _createInstanceWithConstructorInjections( + constructorInjections: any[], + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): any { + let result: any; + + result = new constr(...constructorInjections); + result = _injectProperties(result, childRequests, resolveRequest); + + return result; +} + +async function _createInstanceWithConstructorInjectionsAsync( + constructorInjections: (any | Promise)[], + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): Promise { + return _createInstanceWithConstructorInjections( + await Promise.all(constructorInjections), + constr, + childRequests, + resolveRequest, + ); +} + +function _filterRequestsByTargetType(requests: interfaces.Request[], type: interfaces.TargetType): interfaces.Request[] { + return requests.filter((request: interfaces.Request) => + (request.target !== null && request.target.type === type)); +} + +function _getConstructionInjections(childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler): any[] { + const constructorInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ConstructorArgument); + + return constructorInjectionsRequests.map(resolveRequest); +} + +function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: any): any { + + const postConstructResult = _postConstruct(constr, result); + + if (isPromise(postConstructResult)) { + return postConstructResult.then(() => result); + } else { + return result; + } } function _postConstruct(constr: interfaces.Newable, result: any): void | Promise { @@ -44,12 +113,7 @@ function _postConstruct(constr: interfaces.Newable, result: any): void | Pr } } -function resolveInstance( - binding: interfaces.Binding, - constr: interfaces.Newable, - childRequests: interfaces.Request[], - resolveRequest: interfaces.ResolveRequestHandler -): any { +function _validateInstanceResolution(binding: interfaces.Binding, constr: interfaces.Newable): void { if (binding.scope === "Transient") { if (typeof binding.onDeactivation === "function") { throw new Error(ON_DEACTIVATION_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); @@ -59,46 +123,23 @@ function resolveInstance( throw new Error(PRE_DESTROY_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); } } +} - let result: any = null; - - if (childRequests.length > 0) { - const constructorInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => - (childRequest.target !== null && childRequest.target.type === TargetTypeEnum.ConstructorArgument)); - - const constructorInjections = constructorInjectionsRequests.map(resolveRequest); - - if (constructorInjections.some(isPromise)) { - return new Promise( async (resolve, reject) => { - try { - const resolved = await Promise.all(constructorInjections); - - result = _createInstance(constr, resolved); - result = _injectProperties(result, childRequests, resolveRequest); - - await _postConstruct(constr, result); - - resolve(result); - } catch (ex) { - reject(ex); - } - }); - } +function resolveInstance( + binding: interfaces.Binding, + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): any { + _validateInstanceResolution(binding, constr); - result = _createInstance(constr, constructorInjections); - result = _injectProperties(result, childRequests, resolveRequest); + const result = _createInstance(constr, childRequests, resolveRequest); + if (isPromise(result)) { + return result.then((resolvedResult) => _getInstanceAfterPostConstruct(constr, resolvedResult)); } else { - result = new constr(); + return _getInstanceAfterPostConstruct(constr, result); } - - const post = _postConstruct(constr, result); - - if (isPromise(post)) { - return post.then(() => result); - } - - return result; } export { resolveInstance }; diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index dbc2f137f..efa2a0ab3 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -114,7 +114,11 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => throw new Error(`${ERROR_MSGS.INVALID_BINDING_TYPE} ${serviceIdentifier}`); } - result = onActivation(request, binding, result); + if (isPromise(result)) { + result = result.then((resolved) => _onActivation(request, binding, resolved)); + } else { + result = _onActivation(request, binding, result); + } // store in cache if scope is singleton if (isSingleton) { @@ -122,7 +126,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => binding.activated = true; if (isPromise(result)) { - result = result.catch((ex) => { + result = result.catch((ex) => { // allow binding to retry in future binding.cache = null; binding.activated = false; @@ -145,78 +149,146 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => }; -function onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T | Promise): T | Promise { - if (isPromise(resolved)) { - return resolved.then((unpromised) => onActivation(request, binding, unpromised)); - } +function _onActivation(request: interfaces.Request, binding: interfaces.Binding, previousResult: T): T | Promise { + let result = _callBindingActivation(request, binding, previousResult); - let result: T | Promise = resolved; + const containersIterator = _getParentContainersIterator(request.parentContext.container, true); - // use activation handler if available - if (typeof binding.onActivation === "function") { - result = binding.onActivation(request.parentContext, result as T); - } + let container: interfaces.Container; + let containersIteratorResult = containersIterator.next(); - const containers = [request.parentContext.container]; + do { + container = containersIteratorResult.value; - let parent = request.parentContext.container.parent; + if (isPromise(result)) { + result = _callContainerActivationsAsync(request, container, result); + } else { + result = _callContainerActivations(request, container, result); + } - while (parent) { - containers.unshift(parent); + containersIteratorResult = containersIterator.next(); - parent = parent.parent; - } + // make sure if we are currently on the container that owns the binding, not to keep looping down to child containers + } while (containersIteratorResult.done !== true && !getBindingDictionary(container).hasKey(request.serviceIdentifier)); - const iter = containers.entries(); + return result; +} - return activationLoop(request.parentContext, iter.next().value[1], iter, binding, request.serviceIdentifier, result); +const _callBindingActivation = (request: interfaces.Request, binding: interfaces.Binding, previousResult: T): T | Promise => { + let result: T | Promise; + + // use activation handler if available + if (typeof binding.onActivation === "function") { + result = binding.onActivation(request.parentContext, previousResult); + } else { + result = previousResult; + } + + return result; } -function activationLoop( +const _callActivations = ( + activationsIterator: Iterator>, context: interfaces.Context, - container: interfaces.Container, - containerIterator: IterableIterator<[number, interfaces.Container]>, - binding: interfaces.Binding, - identifier: interfaces.ServiceIdentifier, - previous: T | Promise, - iterator?: IterableIterator<[number, interfaces.BindingActivation]> - ): T | Promise { - if (isPromise(previous)) { - return previous.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised)); + result: T, +): T | Promise => { + let activation = activationsIterator.next(); + + while (!activation.done) { + result = activation.value(context, result); + + if (isPromise(result)) { + return result.then((resolved) => _callActivationsAsync(activationsIterator, context, resolved)); + } + + activation = activationsIterator.next(); } - let result = previous; + return result; +} - let iter = iterator; +const _callActivationsAsync = async( + activationsIterator: Iterator>, + context: interfaces.Context, + result: T, +): Promise => { + let activation = activationsIterator.next(); - if (!iter) { - // smell accessing _activations, but similar pattern is done in planner.getBindingDictionary() - const activations = (container as any)._activations as interfaces.Lookup>; + while (!activation.done) { + result = await activation.value(context, result); - iter = activations.hasKey(identifier) ? activations.get(identifier).entries() : [].entries(); + activation = activationsIterator.next(); } - let next = iter.next(); + return result; +} + +const _callContainerActivations = ( + request: interfaces.Request, + container: interfaces.Container, + previousResult: T, +): T | Promise => { + const context = request.parentContext; + const serviceIdentifier = request.serviceIdentifier; + + const activationsIterator = _extractActivationsForService(container, serviceIdentifier); - while (!next.done) { - result = next.value[1](context, result); + const activationsTraverseResult = _callActivations(activationsIterator, context, previousResult); - if (isPromise(result)) { - return result.then((unpromised) => activationLoop(context, container, containerIterator, binding, identifier, unpromised, iter)); - } + return activationsTraverseResult; +} + +const _callContainerActivationsAsync = async ( + request: interfaces.Request, + container: interfaces.Container, + previousResult: T | Promise, +): Promise => { + const context = request.parentContext; + const serviceIdentifier = request.serviceIdentifier; - next = iter.next(); + const activationsIterator = _extractActivationsForService(container, serviceIdentifier); + + return _callActivationsAsync(activationsIterator, context, await previousResult); +} + +const _extractActivationsForService = (container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier) => { + // smell accessing _activations, but similar pattern is done in planner.getBindingDictionary() + const activations = (container as any)._activations as interfaces.Lookup>; + + return activations.hasKey(serviceIdentifier) ? activations.get(serviceIdentifier).values() : [].values(); +} + +const _getParentContainersIterator = (container: interfaces.Container, includeSelf: boolean = false): Iterator => { + const containersStack: interfaces.Container[] = []; + + if (includeSelf) { + containersStack.push(container); } - const nextContainer = containerIterator.next(); + let parent = container.parent; + + while (parent !== null) { + containersStack.push(parent); - if (nextContainer.value && !getBindingDictionary(container).hasKey(identifier)) { - // make sure if we are currently on the container that owns the binding, not to keep looping down to child containers - return activationLoop(context, nextContainer.value[1], containerIterator, binding, identifier, result); + parent = parent.parent; } - return result; - } + const getNextContainer: () => IteratorResult = () => { + const nextContainer = containersStack.pop(); + + if (nextContainer !== undefined) { + return { done: false, value: nextContainer }; + } else { + return { done: true, value: undefined }; + } + }; + + const containersIterator: Iterator = { + next: getNextContainer, + }; + + return containersIterator; +} function resolve(context: interfaces.Context): T { const _f = _resolveRequest(context.plan.rootRequest.requestScope); diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index 1e07bdd51..cea4293fc 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -152,4 +152,23 @@ describe("ContainerModule", () => { expect(deact).eql(true); }); + + it("Should be able to add an async deactivation hook through a container module (async)", async () => { + const container = new Container(); + container.bind("A").toConstantValue("1"); + + let deact = false; + + const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + onDeactivation("A", async () => { + deact = true; + }); + }); + + container.load(warriors); + container.get("A"); + await container.unbindAsync("A"); + + expect(deact).eql(true); + }); }); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 5029db138..a8d6a6b94 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -2002,7 +2002,7 @@ describe("Resolve", () => { expect(result).equals(constructed); }); - it("Should priortize onActivation of parent container over child container", async () => { + it("Should priortize onActivation of parent container over child container", () => { const container = new Container(); container.onActivation("foo", (context, previous) => `${previous}baz`); container.onActivation("foo", (context, previous) => `${previous}1`); @@ -2018,6 +2018,22 @@ describe("Resolve", () => { expect(result).equals("barbahbaz1bum2"); }); + it("Should priortize async onActivation of parent container over child container (async)", async () => { + const container = new Container(); + container.onActivation("foo", async (context, previous) => `${previous}baz`); + container.onActivation("foo", async (context, previous) => `${previous}1`); + + const child = container.createChild(); + + child.bind("foo").toConstantValue("bar").onActivation((c, previous) => `${previous}bah`); + child.onActivation("foo", async (context, previous) => `${previous}bum`); + child.onActivation("foo", async (context, previous) => `${previous}2`); + + const result = await child.getAsync("foo"); + + expect(result).equals("barbahbaz1bum2"); +}); + it("Should not allow onActivation of parent on child container", async () => { class Parent { diff --git a/tsconfig.json b/tsconfig.json index a3af07e86..b58ead8e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,4 +24,4 @@ "noUnusedLocals": true, "strictNullChecks": true } -} \ No newline at end of file +} From 08ad622bd7132765534f445af834f6115dfe10ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 14 Apr 2021 09:59:57 +0200 Subject: [PATCH 24/64] refactor: update error messages to not to receive any type parameters --- src/constants/error_msgs.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/constants/error_msgs.ts b/src/constants/error_msgs.ts index 9895c77ea..01bbd26e1 100644 --- a/src/constants/error_msgs.ts +++ b/src/constants/error_msgs.ts @@ -17,7 +17,7 @@ export const INVALID_BINDING_TYPE = "Invalid binding type:"; export const NO_MORE_SNAPSHOTS_AVAILABLE = "No snapshot available to restore."; export const INVALID_MIDDLEWARE_RETURN = "Invalid return type in middleware. Middleware must return!"; export const INVALID_FUNCTION_BINDING = "Value provided to function binding must be a function!"; -export const LAZY_IN_SYNC = (key: any) => `You are attempting to construct '${key}' in a synchronous way +export const LAZY_IN_SYNC = (key: unknown) => `You are attempting to construct '${key}' in a synchronous way but it has asynchronous dependencies.`; export const INVALID_TO_SELF_VALUE = "The toSelf function can only be applied when a constructor is " + @@ -45,11 +45,11 @@ export const MULTIPLE_PRE_DESTROY_METHODS = "Cannot apply @preDestroy decorator export const MULTIPLE_POST_CONSTRUCT_METHODS = "Cannot apply @postConstruct decorator multiple times in the same class"; export const ASYNC_UNBIND_REQUIRED = "Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"; export const POST_CONSTRUCT_ERROR = (...values: any[]) => `@postConstruct error in class ${values[0]}: ${values[1]}`; -export const PRE_DESTROY_ERROR = (...values: any[]) => `@preDestroy error in class ${values[0]}: ${values[1]}`; -export const ON_DEACTIVATION_ERROR = (...values: any[]) => `onDeactivation() error in class ${values[0]}: ${values[1]}`; +export const PRE_DESTROY_ERROR = (clazz: string, errorMessage: string) => `@preDestroy error in class ${clazz}: ${errorMessage}`; +export const ON_DEACTIVATION_ERROR = (clazz: string, errorMessage: string) => `onDeactivation() error in class ${clazz}: ${errorMessage}`; -export const CIRCULAR_DEPENDENCY_IN_FACTORY = (...values: any[]) => "It looks like there is a circular dependency " + - `in one of the '${values[0]}' bindings. Please investigate bindings with` + - `service identifier '${values[1]}'.`; +export const CIRCULAR_DEPENDENCY_IN_FACTORY = (factoryType: string, serviceIdentifier: string) => + `It looks like there is a circular dependency in one of the '${factoryType}' bindings. Please investigate bindings with` + + `service identifier '${serviceIdentifier}'.`; export const STACK_OVERFLOW = "Maximum call stack size exceeded"; From b792336932730c3b8de53e7b231a481271776a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 14 Apr 2021 10:24:18 +0200 Subject: [PATCH 25/64] refactor: update instantiation with better typings --- src/resolution/instantiation.ts | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index e644358c9..ab8d6d532 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -5,32 +5,32 @@ import { interfaces } from "../interfaces/interfaces"; import { Metadata } from "../planning/metadata"; import { isPromise } from "../utils/async"; -function _injectProperties( - instance: any, +function _injectProperties( + instance: T, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler -): any { +): T { const propertyInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ClassProperty); const propertyInjections = propertyInjectionsRequests.map(resolveRequest); propertyInjectionsRequests.forEach((r: interfaces.Request, index: number) => { - let propertyName = ""; - propertyName = r.target.name.value(); + const propertyName = r.target.name.value(); const injection = propertyInjections[index]; - instance[propertyName] = injection; + + (instance as Record)[propertyName] = injection; }); return instance; } -function _createInstance( - constr: interfaces.Newable, +function _createInstance( + constr: interfaces.Newable, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler, -): any { - let result: any; +): T | Promise { + let result: T | Promise; if (childRequests.length > 0) { const constructorInjections = _getConstructionInjections(childRequests, resolveRequest); @@ -52,13 +52,13 @@ function _createInstance( return result; } -function _createInstanceWithConstructorInjections( - constructorInjections: any[], - constr: interfaces.Newable, +function _createInstanceWithConstructorInjections( + constructorInjections: unknown[], + constr: interfaces.Newable, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler, -): any { - let result: any; +): T { + let result: T; result = new constr(...constructorInjections); result = _injectProperties(result, childRequests, resolveRequest); @@ -66,12 +66,12 @@ function _createInstanceWithConstructorInjections( return result; } -async function _createInstanceWithConstructorInjectionsAsync( - constructorInjections: (any | Promise)[], - constr: interfaces.Newable, +async function _createInstanceWithConstructorInjectionsAsync( + constructorInjections: (unknown | Promise)[], + constr: interfaces.Newable, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler, -): Promise { +): Promise { return _createInstanceWithConstructorInjections( await Promise.all(constructorInjections), constr, @@ -85,13 +85,13 @@ function _filterRequestsByTargetType(requests: interfaces.Request[], type: inter (request.target !== null && request.target.type === type)); } -function _getConstructionInjections(childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler): any[] { +function _getConstructionInjections(childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler): unknown[] { const constructorInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ConstructorArgument); return constructorInjectionsRequests.map(resolveRequest); } -function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: any): any { +function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: T): T | Promise { const postConstructResult = _postConstruct(constr, result); @@ -102,18 +102,18 @@ function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: } } -function _postConstruct(constr: interfaces.Newable, result: any): void | Promise { +function _postConstruct(constr: interfaces.Newable, instance: T): void | Promise { if (Reflect.hasMetadata(METADATA_KEY.POST_CONSTRUCT, constr)) { const data: Metadata = Reflect.getMetadata(METADATA_KEY.POST_CONSTRUCT, constr); try { - return result[data.value](); + return (instance as T & Record void>)[data.value](); } catch (e) { throw new Error(POST_CONSTRUCT_ERROR(constr.name, e.message)); } } } -function _validateInstanceResolution(binding: interfaces.Binding, constr: interfaces.Newable): void { +function _validateInstanceResolution(binding: interfaces.Binding, constr: interfaces.Newable): void { if (binding.scope === "Transient") { if (typeof binding.onDeactivation === "function") { throw new Error(ON_DEACTIVATION_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); @@ -125,12 +125,12 @@ function _validateInstanceResolution(binding: interfaces.Binding, constr: i } } -function resolveInstance( - binding: interfaces.Binding, - constr: interfaces.Newable, +function resolveInstance( + binding: interfaces.Binding, + constr: interfaces.Newable, childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler, -): any { +): T | Promise { _validateInstanceResolution(binding, constr); const result = _createInstance(constr, childRequests, resolveRequest); From a3b7c9d68a1622f6afdcd74fa5620e76fe9f4dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 14 Apr 2021 10:29:45 +0200 Subject: [PATCH 26/64] refactor: update Lookup.clone to use a Typeguard --- src/container/lookup.ts | 5 +++-- src/utils/clonable.ts | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 src/utils/clonable.ts diff --git a/src/container/lookup.ts b/src/container/lookup.ts index d2fdf54b1..0ac442938 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -1,7 +1,8 @@ import * as ERROR_MSGS from "../constants/error_msgs"; import { interfaces } from "../interfaces/interfaces"; +import { isClonable } from "../utils/clonable"; -class Lookup | any> implements interfaces.Lookup { +class Lookup implements interfaces.Lookup { // dictionary used store multiple values for each key private _map: Map, T[]>; @@ -91,7 +92,7 @@ class Lookup | any> implements interfaces.Loo const copy = new Lookup(); this._map.forEach((value, key) => { - value.forEach((b: any) => copy.add(key, b.clone ? b.clone() : b)); + value.forEach((b) => copy.add(key, isClonable(b) ? b.clone() : b)); }); return copy; diff --git a/src/utils/clonable.ts b/src/utils/clonable.ts new file mode 100644 index 000000000..8a2f9adca --- /dev/null +++ b/src/utils/clonable.ts @@ -0,0 +1,10 @@ +import { interfaces } from "../interfaces/interfaces"; + +function isClonable(obj: unknown): obj is interfaces.Clonable { + return (typeof obj === 'object') + && (obj !== null) + && ('clone' in obj) + && typeof (obj as interfaces.Clonable).clone === 'function'; +} + +export { isClonable }; \ No newline at end of file From f94f12590e05fc0db14b7a7bb9544ddc0783e299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 14 Apr 2021 10:42:08 +0200 Subject: [PATCH 27/64] refactor: update error msg with non any parameters --- src/constants/error_msgs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/error_msgs.ts b/src/constants/error_msgs.ts index 01bbd26e1..3935b06b6 100644 --- a/src/constants/error_msgs.ts +++ b/src/constants/error_msgs.ts @@ -44,7 +44,7 @@ export const CONTAINER_OPTIONS_INVALID_SKIP_BASE_CHECK = "Invalid Container opti export const MULTIPLE_PRE_DESTROY_METHODS = "Cannot apply @preDestroy decorator multiple times in the same class"; export const MULTIPLE_POST_CONSTRUCT_METHODS = "Cannot apply @postConstruct decorator multiple times in the same class"; export const ASYNC_UNBIND_REQUIRED = "Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"; -export const POST_CONSTRUCT_ERROR = (...values: any[]) => `@postConstruct error in class ${values[0]}: ${values[1]}`; +export const POST_CONSTRUCT_ERROR = (clazz: string, errorMessage: string) => `@postConstruct error in class ${clazz}: ${errorMessage}`; export const PRE_DESTROY_ERROR = (clazz: string, errorMessage: string) => `@preDestroy error in class ${clazz}: ${errorMessage}`; export const ON_DEACTIVATION_ERROR = (clazz: string, errorMessage: string) => `onDeactivation() error in class ${clazz}: ${errorMessage}`; From bcf088020fd0216f52b1b8a2986a17a101d86df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Fri, 16 Apr 2021 15:54:51 +0200 Subject: [PATCH 28/64] fix: update onActivation test to use a binding with a transient scope instead of a singleton scope one --- test/container/container_module.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index cea4293fc..c75e43328 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -121,7 +121,7 @@ describe("ContainerModule", () => { it("Should be able to add an activation hook through a container module", () => { const container = new Container(); - container.bind("A").toConstantValue("1"); + container.bind("A").toDynamicValue(() => "1"); expect(container.get("A")).to.eql("1"); const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation) => { From f7a064e9faf209a89243175d667f5b006bd0995e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Mon, 19 Apr 2021 23:12:43 +0200 Subject: [PATCH 29/64] feat: update container._propagateDeactivation to not to _triggerOnDeactivationAndDestroyMetadata multiple times on an async context --- src/container/container.ts | 6 +--- test/container/container.test.ts | 16 ++++++++++ test/container/container_module.test.ts | 24 ++++++++++++++ test/resolution/resolver.test.ts | 42 +++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 7fbfe5d3b..53bbdaea9 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -632,11 +632,7 @@ class Container implements interfaces.Container { constructor: any ): void | Promise { if (this.parent) { - const parentDeactivationResult = this._doDeactivation.bind(this.parent)(binding, instance); - - if (isPromise(parentDeactivationResult)) { - return this._triggerOnDeactivationAndDestroyMetadataAsync(binding, instance, constructor); - } + return this._doDeactivation.bind(this.parent)(binding, instance); } else { return this._triggerOnDeactivationAndDestroyMetadata(binding, instance, constructor); } diff --git a/test/container/container.test.ts b/test/container/container.test.ts index a9297f0e2..679613d6f 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -977,4 +977,20 @@ describe("Container", () => { expect(await container.getTaggedAsync(zero, isValidDivisor, true)).to.equal(1); }); + + it("should be able to get all the servides binded (async)", async () => { + const serviceIdentifier = "service-identifier"; + + const container = new Container(); + + const firstValueBinded = "value-one"; + const secondValueBinded = "value-two"; + + container.bind(serviceIdentifier).toConstantValue(firstValueBinded); + container.bind(serviceIdentifier).toConstantValue(secondValueBinded); + + const services = await container.getAllAsync(serviceIdentifier); + + expect(services).to.deep.eq([firstValueBinded, secondValueBinded]); + }) }); diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index c75e43328..888e1c4ee 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import * as sinon from "sinon"; import { Container } from "../../src/container/container"; import { AsyncContainerModule, ContainerModule } from "../../src/container/container_module"; import { interfaces } from "../../src/interfaces/interfaces"; @@ -171,4 +172,27 @@ describe("ContainerModule", () => { expect(deact).eql(true); }); + + it("Should be able to add multiple async deactivation hook through a container module (async)", async () => { + + const onActivationHandlerSpy = sinon.spy<() => Promise>(async () => undefined); + + const serviceIdentifier = "destroyable"; + const container = new Container(); + + const containerModule = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + onDeactivation(serviceIdentifier, onActivationHandlerSpy); + onDeactivation(serviceIdentifier, onActivationHandlerSpy); + }); + + container.bind(serviceIdentifier).toConstantValue(serviceIdentifier); + + container.get(serviceIdentifier); + + container.load(containerModule); + + await container.unbindAllAsync(); + + expect(onActivationHandlerSpy.callCount).to.eq(2); + }); }); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index c7c4abbe6..459575597 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1589,6 +1589,48 @@ describe("Resolve", () => { expect(klass).eql(4); }); + it("Should invoke destory in order (all async unless child deactivation): child container, parent container, binding, class", async () => { + let roll = 1; + let binding = null; + let klass = null; + let parent = null; + let child = null; + + @injectable() + class Destroyable { + @preDestroy() + public async myPreDestroyMethod() { + klass = roll; + roll += 1; + } + } + + const container = new Container(); + container.onDeactivation("Destroyable", async () => { + parent = roll; + roll += 1; + }); + + const childContainer = container.createChild(); + childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => { + binding = roll; + roll += 1; + }); + childContainer.onDeactivation("Destroyable", () => { + child = roll; + roll += 1; + }); + + childContainer.get("Destroyable"); + await childContainer.unbindAsync("Destroyable"); + + expect(roll).eql(5); + expect(child).eql(1); + expect(parent).eql(2); + expect(binding).eql(3); + expect(klass).eql(4); +}); + it("Should force a class with an async pre destroy to use the async unbind api", async () => { @injectable() class Destroyable { From 9f40b2e50119e9990183c26df75499b6bf0da507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Mon, 19 Apr 2021 23:16:23 +0200 Subject: [PATCH 30/64] style: make it name shorter --- test/resolution/resolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 459575597..e5715a30e 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1589,7 +1589,7 @@ describe("Resolve", () => { expect(klass).eql(4); }); - it("Should invoke destory in order (all async unless child deactivation): child container, parent container, binding, class", async () => { + it("Should invoke destory in order (async): child container, parent container, binding, class", async () => { let roll = 1; let binding = null; let klass = null; From f75be4d9a945662dfcbe3300a74a0a11c85c77b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 20 Apr 2021 21:50:04 +0200 Subject: [PATCH 31/64] docs: added get async docs --- wiki/container_api.md | 134 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/wiki/container_api.md b/wiki/container_api.md index 931b5bbf0..58855ce94 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -99,9 +99,35 @@ expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Samurai).name).to.eql("S expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Katana).name).to.eql("Katana"); ``` +## container.get() + +Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: + +```ts +let container = new Container(); +container.bind("Weapon").to(Katana); + +let katana = container.get("Weapon"); +``` + +## container.getAsync() + +Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding, otherwise an error is thrown: + +```ts +async function buildLevel1(): Level1 { + return new Level1(); +} + +let container = new Container(); +container.bind("Level1").toDynamicValue(() => buildLevel1()); + +let level1 = await container.getAsync("Level1"); // Returns Promise +``` + ## container.getNamed() -Named bindings: +Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -112,9 +138,22 @@ let katana = container.getNamed("Weapon", "japanese"); let shuriken = container.getNamed("Weapon", "chinese"); ``` +## container.getNamedAsync() + +Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: + +```ts +let container = new Container(); +container.bind("Weapon").toDynamicValue(async () => new Katana()).whenTargetNamed("japanese"); +container.bind("Weapon").toDynamicValue(async () => new Weapon()).whenTargetNamed("chinese"); + +let katana = container.getNamedAsync("Weapon", "japanese"); +let shuriken = container.getNamedAsync("Weapon", "chinese"); +``` + ## container.getTagged() -Tagged bindings: +Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -125,9 +164,22 @@ let katana = container.getTagged("Weapon", "faction", "samurai"); let shuriken = container.getTagged("Weapon", "faction", "ninja"); ``` +## container.getTaggedAsync() + +Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: + +```ts +let container = new Container(); +container.bind("Weapon").toDynamicValue(async () => new Katana()).whenTargetTagged("faction", "samurai"); +container.bind("Weapon").toDynamicValue(async () => new Weapon()).whenTargetTagged("faction", "ninja"); + +let katana = container.getTaggedAsync("Weapon", "faction", "samurai"); +let shuriken = container.getTaggedAsync("Weapon", "faction", "ninja"); +``` + ## container.getAll() -Get all available bindings for a given identifier: +Get all available bindings for a given identifier. All the bindings must be syncronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -137,10 +189,21 @@ container.bind("Weapon").to(Shuriken); let weapons = container.getAll("Weapon"); // returns Weapon[] ``` +## container.getAllAsync() + +Get all available bindings for a given identifier: + +```ts +let container = new Container(); +container.bind("Weapon").to(Katana); +container.bind("Weapon").toDynamicValue(async () => new Shuriken()); + +let weapons = await container.getAllAsync("Weapon"); // returns Promise +``` + ## container.getAllNamed() -Get all available bindings for a given identifier that match the given -named constraint: +Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be syncronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -167,11 +230,39 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` +## container.getAllNamedAsync() + +Resolves all the dependencies by its runtime identifier that matches the given named constraint: + +```ts +let container = new Container(); + +interface Intl { + hello?: string; + goodbye?: string; +} + +container.bind("Intl").toDynamicValue(async () => ({ hello: "bonjour" })).whenTargetNamed("fr"); +container.bind("Intl").toDynamicValue(async () => ({ goodbye: "au revoir" })).whenTargetNamed("fr"); + +container.bind("Intl").toDynamicValue(async () => ({ hello: "hola" })).whenTargetNamed("es"); +container.bind("Intl").toDynamicValue(async () => ({ goodbye: "adios" })).whenTargetNamed("es"); + +let fr = container.getAllNamedAsync("Intl", "fr"); +expect(fr.length).to.eql(2); +expect(fr[0].hello).to.eql("bonjour"); +expect(fr[1].goodbye).to.eql("au revoir"); + +let es = container.getAllNamedAsync("Intl", "es"); +expect(es.length).to.eql(2); +expect(es[0].hello).to.eql("hola"); +expect(es[1].goodbye).to.eql("adios"); +``` + ## container.getAllTagged() -Get all available bindings for a given identifier that match the given -named constraint: +Resolves all the dependencies by its runtime identifier that matches the given tagged constraint. All the binding must be syncronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -198,6 +289,35 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` +## container.getAllTaggedAsync() + +Resolves all the dependencies by its runtime identifier that matches the given tagged constraint: + +```ts +let container = new Container(); + +interface Intl { + hello?: string; + goodbye?: string; +} + +container.bind("Intl").toDynamicValue(async () => ({ hello: "bonjour" })).whenTargetTagged("lang", "fr"); +container.bind("Intl").toDynamicValue(async () => ({ goodbye: "au revoir" })).whenTargetTagged("lang", "fr"); + +container.bind("Intl").toDynamicValue(async () => ({ hello: "hola" })).whenTargetTagged("lang", "es"); +container.bind("Intl").toDynamicValue(async () => ({ goodbye: "adios" })).whenTargetTagged("lang", "es"); + +let fr = container.getAllTaggedAsync("Intl", "lang", "fr"); +expect(fr.length).to.eql(2); +expect(fr[0].hello).to.eql("bonjour"); +expect(fr[1].goodbye).to.eql("au revoir"); + +let es = container.getAllTaggedAsync("Intl", "lang", "es"); +expect(es.length).to.eql(2); +expect(es[0].hello).to.eql("hola"); +expect(es[1].goodbye).to.eql("adios"); +``` + ## container.isBound(serviceIdentifier: ServiceIdentifier) You can use the `isBound` method to check if there are registered bindings for a given service identifier. From 9f17834bf94169bebc57be3cf7aa7551e97ed97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 21 Apr 2021 16:59:45 +0200 Subject: [PATCH 32/64] docs: update container docs --- wiki/container_api.md | 53 +++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/wiki/container_api.md b/wiki/container_api.md index 58855ce94..76f0e3195 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -99,7 +99,7 @@ expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Samurai).name).to.eql("S expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Katana).name).to.eql("Katana"); ``` -## container.get() +## container.get(serviceIdentifier: ServiceIdentifier) Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -110,7 +110,7 @@ container.bind("Weapon").to(Katana); let katana = container.get("Weapon"); ``` -## container.getAsync() +## container.getAsync(serviceIdentifier: ServiceIdentifier) Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -125,7 +125,7 @@ container.bind("Level1").toDynamicValue(() => buildLevel1()); let level1 = await container.getAsync("Level1"); // Returns Promise ``` -## container.getNamed() +## container.getNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol) Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -138,7 +138,7 @@ let katana = container.getNamed("Weapon", "japanese"); let shuriken = container.getNamed("Weapon", "chinese"); ``` -## container.getNamedAsync() +## container.getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol) Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -151,7 +151,7 @@ let katana = container.getNamedAsync("Weapon", "japanese"); let shuriken = container.getNamedAsync("Weapon", "chinese"); ``` -## container.getTagged() +## container.getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -164,7 +164,7 @@ let katana = container.getTagged("Weapon", "faction", "samurai"); let shuriken = container.getTagged("Weapon", "faction", "ninja"); ``` -## container.getTaggedAsync() +## container.getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -177,7 +177,7 @@ let katana = container.getTaggedAsync("Weapon", "faction", "samurai"); let shuriken = container.getTaggedAsync("Weapon", "faction", "ninja"); ``` -## container.getAll() +## container.getAll(serviceIdentifier: interfaces.ServiceIdentifier) Get all available bindings for a given identifier. All the bindings must be syncronously resolved, otherwise an error is thrown: @@ -189,7 +189,7 @@ container.bind("Weapon").to(Shuriken); let weapons = container.getAll("Weapon"); // returns Weapon[] ``` -## container.getAllAsync() +## container.getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier) Get all available bindings for a given identifier: @@ -201,7 +201,7 @@ container.bind("Weapon").toDynamicValue(async () => new Shuriken()); let weapons = await container.getAllAsync("Weapon"); // returns Promise ``` -## container.getAllNamed() +## container.getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol) Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be syncronously resolved, otherwise an error is thrown: @@ -230,7 +230,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllNamedAsync() +## container.getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol) Resolves all the dependencies by its runtime identifier that matches the given named constraint: @@ -260,7 +260,7 @@ expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTagged() +## container.getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) Resolves all the dependencies by its runtime identifier that matches the given tagged constraint. All the binding must be syncronously resolved, otherwise an error is thrown: @@ -289,7 +289,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTaggedAsync() +## container.getAllTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) Resolves all the dependencies by its runtime identifier that matches the given tagged constraint: @@ -454,3 +454,32 @@ expect(ninja.fight()).to.eql("cut!"); ``` Please note that it only allows to skip declaring a binding for the root element in the dependency graph (composition root). All the sub-dependencies (e.g. `Katana` in the preceding example) will require a binding to be declared. + +## container.onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation) + +Adds an activation handler for the dependencies's identifier. + +```ts +let container = new Container(); +container.bind("Weapon").to(Katana); +container.onActivation("Weapon", (context: interfaces.Context, katana: Katana): Katana | Promise => { + console.log('katana instance activation!'); + return katana; +}); + +let katana = container.get("Weapon"); +``` + +## onDeactivation(serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) + +Adds a deactivation handler for the dependencie's identifier. + +```ts +let container = new Container(); +container.bind("Weapon").to(Katana); +container.onDeactivation("Weapon", (katana: Katana): void | Promise => { + console.log('katana instance deactivation!'); +}); + +container.unbind("Weapon"); +``` From b2d8e61a65986f043cd78c53833edf457d644cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 21 Apr 2021 20:59:06 +0200 Subject: [PATCH 33/64] docs: add activation handler and pre destroy docs --- wiki/activation_handler.md | 14 ++++++++------ wiki/pre_destroy.md | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 wiki/pre_destroy.md diff --git a/wiki/activation_handler.md b/wiki/activation_handler.md index 91b75c7e9..c6c7b0cf1 100644 --- a/wiki/activation_handler.md +++ b/wiki/activation_handler.md @@ -1,11 +1,6 @@ # Activation handler -It is possible to add an activation handler for a type. The activation handler is -invoked after a dependency has been resolved and before it is added to the cache -(if singleton) and injected. This is useful to keep our dependencies agnostic of -the implementation of crosscutting concerns like caching or logging. The following -example uses a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) -to intercept one of the methods (`use`) of a dependency (`Katana`). +It is possible to add an activation handler for a type. The activation handler is invoked after a dependency has been resolved and before it is added to the cache (if singleton) and injected. This is useful to keep our dependencies agnostic of the implementation of crosscutting concerns like caching or logging. The following example uses a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to intercept one of the methods (`use`) of a dependency (`Katana`). ```ts interface Katana { @@ -56,3 +51,10 @@ ninja.katana.use(); > Used Katana! > Finished: 1457895135762 ``` + +There are multiple ways to provide an activation handler + +- Adding the handler to the container +- Adding the handler to the binding + +When multiple activation handlers are binded to a service identifier, the bindind handlers are called before any others. Any handler defined in a container is called before a handler defined in it's parent container diff --git a/wiki/pre_destroy.md b/wiki/pre_destroy.md new file mode 100644 index 000000000..a17f74edd --- /dev/null +++ b/wiki/pre_destroy.md @@ -0,0 +1,20 @@ +# Pre Destroy Decorator + +It is possible to add a **@preDestroy** decorator for a class method. This decorator will run before a service is unbinded for any cached instance. For this reason, classes related to bindings on transient scope can not contain a method with this decorator sice there is no way to know which instances should be affected. + +```ts +@injectable() +class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + console.log('Destroyable is about to be unbinded!'); + } +} + +const container = new Container(); +container.bind("Destroyable").to(Destroyable).inSingletonScope(); + +container.get("Destroyable"); + +container.unbindAll(); +``` From 292bab9a205d021204c65856eadba864f2fc6448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 21 Apr 2021 22:57:33 +0200 Subject: [PATCH 34/64] add missing types and refactor types to use BindingActivation and BindingDeactivation --- src/bindings/binding.ts | 4 ++-- src/interfaces/interfaces.ts | 2 ++ src/syntax/binding_on_syntax.ts | 4 ++-- src/utils/clonable.ts | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/bindings/binding.ts b/src/bindings/binding.ts index fd218baec..db1849564 100644 --- a/src/bindings/binding.ts +++ b/src/bindings/binding.ts @@ -40,10 +40,10 @@ class Binding implements interfaces.Binding { public constraint: (request: interfaces.Request) => boolean; // On activation handler (invoked just before an instance is added to cache and injected) - public onActivation: ((context: interfaces.Context, injectable: T) => T | Promise) | null; + public onActivation: interfaces.BindingActivation | null; // On deactivation handler (invoked just before an instance is unbinded and removed from container) - public onDeactivation: ((injectable: T) => Promise | void) | null; + public onDeactivation: interfaces.BindingDeactivation | null; public constructor(serviceIdentifier: interfaces.ServiceIdentifier, scope: interfaces.BindingScope) { this.id = id(); diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 0dfcaf393..bfc9a6f73 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -175,7 +175,9 @@ namespace interfaces { bind(serviceIdentifier: ServiceIdentifier): BindingToSyntax; rebind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax; unbind(serviceIdentifier: ServiceIdentifier): void; + unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise; unbindAll(): void; + unbindAllAsync(): Promise; isBound(serviceIdentifier: ServiceIdentifier): boolean; isBoundNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): boolean; isBoundTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): boolean; diff --git a/src/syntax/binding_on_syntax.ts b/src/syntax/binding_on_syntax.ts index 0489ab0e3..1cfff5863 100644 --- a/src/syntax/binding_on_syntax.ts +++ b/src/syntax/binding_on_syntax.ts @@ -9,12 +9,12 @@ class BindingOnSyntax implements interfaces.BindingOnSyntax { this._binding = binding; } - public onActivation(handler: (context: interfaces.Context, injectable: T) => T | Promise): interfaces.BindingWhenSyntax { + public onActivation(handler: interfaces.BindingActivation): interfaces.BindingWhenSyntax { this._binding.onActivation = handler; return new BindingWhenSyntax(this._binding); } - public onDeactivation(handler: (injectable: T) => void | Promise): interfaces.BindingWhenSyntax { + public onDeactivation(handler: interfaces.BindingDeactivation): interfaces.BindingWhenSyntax { this._binding.onDeactivation = handler; return new BindingWhenSyntax(this._binding); } diff --git a/src/utils/clonable.ts b/src/utils/clonable.ts index 8a2f9adca..ae2c18587 100644 --- a/src/utils/clonable.ts +++ b/src/utils/clonable.ts @@ -7,4 +7,4 @@ function isClonable(obj: unknown): obj is interfaces.Clonable { && typeof (obj as interfaces.Clonable).clone === 'function'; } -export { isClonable }; \ No newline at end of file +export { isClonable }; From 28c5d4edb469978f06db91c00139a1923994f895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 21 Apr 2021 23:04:18 +0200 Subject: [PATCH 35/64] add deactivation_handler docs --- README.md | 1 + wiki/deactivation_handler.md | 79 ++++++++++++++++++++++++++++++++++++ wiki/readme.md | 1 + 3 files changed, 81 insertions(+) create mode 100644 wiki/deactivation_handler.md diff --git a/README.md b/README.md index a4be56eb9..2e356b1db 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ Let's take a look to the InversifyJS features! - [Auto factory](https://github.com/inversify/InversifyJS/blob/master/wiki/auto_factory.md) - [Injecting a Provider (asynchronous Factory)](https://github.com/inversify/InversifyJS/blob/master/wiki/provider_injection.md) - [Activation handler](https://github.com/inversify/InversifyJS/blob/master/wiki/activation_handler.md) +- [Deactivation handler](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md) - [Post Construct decorator](https://github.com/inversify/InversifyJS/blob/master/wiki/post_construct.md) - [Middleware](https://github.com/inversify/InversifyJS/blob/master/wiki/middleware.md) - [Multi-injection](https://github.com/inversify/InversifyJS/blob/master/wiki/multi_injection.md) diff --git a/wiki/deactivation_handler.md b/wiki/deactivation_handler.md new file mode 100644 index 000000000..d1ee194d6 --- /dev/null +++ b/wiki/deactivation_handler.md @@ -0,0 +1,79 @@ +# Deactivation handler + +It is possible to add an activation handler for a type not binded in transient scope. The deactivation handler is invoked before the type is unbinded from the container: + +```ts +@injectable() +class Destroyable { +} + +const container = new Container(); +container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() + .onDeactivation((destroyable: Destroyable) => { + console.log("Destroyable service is about to be unbinded"); + }); + +await container.get("Destroyable"); + +await container.unbind("Destroyable"); +``` + +It's possible to add a deactivation handler in multiple ways + +- Adding the handler to the container. +- Adding the handler to a binding. +- Adding the handler to the class through the [preDestroy decorator](./pre_destroy.md). + +Handlers added to the container are the firsts ones to be resolved. Any handler added to a child container is called before the ones added to their parent. Any handler added through the `preDestroy` decorator is called after any handler added to a container or a biding: + +```ts +let roll = 1; +let binding = null; +let klass = null; +let parent = null; +let child = null; + +@injectable() +class Destroyable { + @preDestroy() + public myPreDestroyMethod() { + return new Promise((presolve) => { + klass = roll; + roll += 1; + presolve({}); + }); + } +} + +const container = new Container(); +container.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + parent = roll; + roll += 1; + presolve(); + }); +}); + +const childContainer = container.createChild(); +childContainer.bind("Destroyable").to(Destroyable).inSingletonScope().onDeactivation(() => new Promise((presolve) => { + binding = roll; + roll += 1; + presolve(); +})); +childContainer.onDeactivation("Destroyable", () => { + return new Promise((presolve) => { + child = roll; + roll += 1; + presolve(); + }); +}); + +childContainer.get("Destroyable"); +await childContainer.unbindAsync("Destroyable"); + +expect(roll).eql(5); +expect(child).eql(1); +expect(parent).eql(2); +expect(binding).eql(3); +expect(klass).eql(4); +``` diff --git a/wiki/readme.md b/wiki/readme.md index 29b567a01..6559b1aa0 100644 --- a/wiki/readme.md +++ b/wiki/readme.md @@ -25,6 +25,7 @@ Welcome to the InversifyJS wiki! - [Auto factory](./auto_factory.md) - [Injecting a Provider (asynchronous Factory)](./provider_injection.md) - [Activation handler](./activation_handler.md) +- [Deactivation handler](./deactivation_handler.md) - [Post Construct decorator](./post_construct.md) - [Middleware](./middleware.md) - [Multi-injection](./multi_injection.md) From 06e66e305ed5fcd5ab574452387eff0334424ae1 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 22 Apr 2021 18:46:26 +0100 Subject: [PATCH 36/64] fix async multi inject and property inject --- src/container/container.ts | 141 +++++++++++++++---------------- src/resolution/instantiation.ts | 128 ++++++++++++++-------------- src/resolution/resolver.ts | 2 +- test/resolution/resolver.test.ts | 135 +++++++++++++++++++++++++---- 4 files changed, 250 insertions(+), 156 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 53bbdaea9..b817d4529 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -13,6 +13,8 @@ import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; import { Lookup } from "./lookup"; +type GetArgs = Omit + class Container implements interfaces.Container { public id: number; @@ -176,7 +178,7 @@ class Container implements interfaces.Container { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - this._preDestroyBindings(bindings); + this._tryDeactivations(bindings); } this._removeServiceFromDictionary(serviceIdentifier); @@ -186,7 +188,7 @@ class Container implements interfaces.Container { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - await this._preDestroyBindingsAsync(bindings); + await this._tryDeactivationsAsync(bindings); } this._removeServiceFromDictionary(serviceIdentifier); @@ -195,7 +197,7 @@ class Container implements interfaces.Container { // Removes all the type bindings from the registry public unbindAll(): void { this._bindingDictionary.traverse((key, value) => { - this._preDestroyBindings(value); + this._tryDeactivations(value); }); this._bindingDictionary = new Lookup>(); @@ -205,7 +207,7 @@ class Container implements interfaces.Container { const promises: Promise[] = []; this._bindingDictionary.traverse((key, value) => { - promises.push(this._preDestroyBindingsAsync(value)); + promises.push(this._tryDeactivationsAsync(value)); }); await Promise.all(promises); @@ -295,27 +297,27 @@ class Container implements interfaces.Container { // The runtime identifier must be associated with only one binding // use getAll when the runtime identifier is associated with multiple bindings public get(serviceIdentifier: interfaces.ServiceIdentifier): T { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, false); + const getArgs = this._getNotAllArgs(serviceIdentifier, false); - return this._getWithAsyncContext(false, defaultArgs) as T; + return this._getButThrowIfAsync(getArgs) as T; } public getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, false); + const getArgs = this._getNotAllArgs(serviceIdentifier, false); - return this._getWithAsyncContext(true, defaultArgs) as Promise; + return this._get(getArgs) as Promise; } public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, false, key, value); + const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); - return this._getWithAsyncContext(false, defaultArgs) as T; + return this._getButThrowIfAsync(getArgs) as T; } public getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, false, key, value); + const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); - return this._getWithAsyncContext(true, defaultArgs) as Promise; + return this._get(getArgs) as Promise; } public getNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T { @@ -329,21 +331,21 @@ class Container implements interfaces.Container { // Resolves a dependency by its runtime identifier // The runtime identifier can be associated with one or multiple bindings public getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] { - const defaultArgs = this._getAllDefaultArgs(serviceIdentifier); + const getArgs = this._getAllArgs(serviceIdentifier); - return this._getWithAsyncContext(false, defaultArgs) as T[]; + return this._getButThrowIfAsync(getArgs) as T[]; } public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise[] { - const defaultArgs = this._getAllDefaultArgs(serviceIdentifier); + const getArgs = this._getAllArgs(serviceIdentifier); - return this._getWithAsyncContext(true, defaultArgs) as Promise[]; + return this._get(getArgs) as Promise[]; } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, true, key, value); + const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return this._getWithAsyncContext(false, defaultArgs) as T[]; + return this._getButThrowIfAsync(getArgs) as T[]; } public getAllTaggedAsync( @@ -351,9 +353,9 @@ class Container implements interfaces.Container { key: string | number | symbol, value: any ): Promise[] { - const defaultArgs = this._getDefaultArgs(serviceIdentifier, true, key, value); + const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return this._getWithAsyncContext(true, defaultArgs) as Promise[]; + return this._get(getArgs) as Promise[]; } public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { @@ -374,7 +376,7 @@ class Container implements interfaces.Container { return tempContainer.get(constructorFunction); } - private _destroyMetadata(constructor: any, instance: any) { + private _preDestroy(constructor: any, instance: any): Promise | void { if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constructor)) { const data: interfaces.Metadata = Reflect.getMetadata(METADATA_KEY.PRE_DESTROY, constructor); @@ -382,25 +384,25 @@ class Container implements interfaces.Container { } } - private _doDeactivation(binding: Binding, instance: T): void | Promise { + private _deactivate(binding: Binding, instance: T): void | Promise { const constructor = Object.getPrototypeOf(instance).constructor; try { if (this._deactivations.hasKey(binding.serviceIdentifier)) { - const result = this._doServiceBindingsDeactivation( + const result = this._deactivateService( instance, this._deactivations.get(binding.serviceIdentifier).values(), ); if (isPromise(result)) { return this._handleDeactivationError( - result.then(() => this._propagateDeactivationAsync(binding, instance, constructor)), + result.then(() => this._propagateServiceDeactivationThenBindingAndPreDestroyAsync(binding, instance, constructor)), constructor ); } } - const propagateDeactivationResult = this._propagateDeactivation(binding, instance, constructor); + const propagateDeactivationResult = this._propagateServiceDeactivationThenBindingAndPreDestroy(binding, instance, constructor); if (isPromise(propagateDeactivationResult)) { return this._handleDeactivationError(propagateDeactivationResult, constructor); @@ -419,7 +421,7 @@ class Container implements interfaces.Container { } - private _doServiceBindingsDeactivation( + private _deactivateService( instance: T, deactivationsIterator: IterableIterator>, ): void | Promise { @@ -430,7 +432,7 @@ class Container implements interfaces.Container { if (isPromise(result)) { return result.then(() => - this._doServicesBindingsDeactivationAsync(instance, deactivationsIterator), + this._deactivateServiceAsync(instance, deactivationsIterator), ); } @@ -438,7 +440,7 @@ class Container implements interfaces.Container { } } - private async _doServicesBindingsDeactivationAsync( + private async _deactivateServiceAsync( instance: T, deactivationsIterator: IterableIterator>, ): Promise { @@ -450,10 +452,6 @@ class Container implements interfaces.Container { } } - private _getDefaultContextInterceptor(): (context: interfaces.Context) => interfaces.Context { - return (context) => context; - } - private _getContainerModuleHelpersFactory() { const setModuleId = (bindingToSyntax: any, moduleId: number) => { @@ -502,69 +500,68 @@ class Container implements interfaces.Container { // Prepares arguments required for resolution and // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve - private _get(defaultArgs: interfaces.NextArgs): (T | T[] | Promise | Promise[]) { + private _get(getArgs: GetArgs): (T | T[] | Promise | Promise[]) { let result: (T | T[] | Promise | Promise[]) | null = null; - + const planAndResolveArgs:interfaces.NextArgs = { + ...getArgs, + contextInterceptor:(context) => context, + targetType: TargetTypeEnum.Variable + } if (this._middleware) { - result = this._middleware(defaultArgs); + result = this._middleware(planAndResolveArgs); if (result === undefined || result === null) { throw new Error(ERROR_MSGS.INVALID_MIDDLEWARE_RETURN); } } else { - result = this._planAndResolve()(defaultArgs); + result = this._planAndResolve()(planAndResolveArgs); } return result; } - private _getWithAsyncContext( - isAsync: boolean, - defaultArgs: interfaces.NextArgs, + private _getButThrowIfAsync( + getArgs: GetArgs, ): (T | T[] | Promise | Promise[]) { - const result = this._get(defaultArgs); + const result = this._get(getArgs); - if (isPromise(result) && !isAsync) { - throw new Error(ERROR_MSGS.LAZY_IN_SYNC(defaultArgs.serviceIdentifier)); + if (isPromise(result)) { + throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); } return result; } - private _getAllDefaultArgs(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.NextArgs { - const defaultArgs: interfaces.NextArgs = { + private _getAllArgs(serviceIdentifier: interfaces.ServiceIdentifier): GetArgs { + const getAllArgs: GetArgs = { avoidConstraints: true, - contextInterceptor: this._getDefaultContextInterceptor(), isMultiInject: true, - targetType: TargetTypeEnum.Variable, serviceIdentifier, }; - return defaultArgs; + return getAllArgs; } - private _getDefaultArgs( + private _getNotAllArgs( serviceIdentifier: interfaces.ServiceIdentifier, isMultiInject: boolean, key?: string | number | symbol, value?: any, - ): interfaces.NextArgs { - const defaultArgs: interfaces.NextArgs = { + ): GetArgs { + const getNotAllArgs: GetArgs = { avoidConstraints: false, - contextInterceptor: this._getDefaultContextInterceptor(), isMultiInject, - targetType: TargetTypeEnum.Variable, serviceIdentifier, key, value, }; - return defaultArgs; + return getNotAllArgs; } // Planner creates a plan and Resolver resolves a plan // one of the jobs of the Container is to links the Planner // with the Resolver and that is what this function is about - private _planAndResolve(): (args: interfaces.NextArgs) => (T | T[]) { + private _planAndResolve(): (args: interfaces.NextArgs) => (T | T[] | Promise | Promise[]) { return (args: interfaces.NextArgs) => { // create a plan @@ -590,21 +587,21 @@ class Container implements interfaces.Container { }; } - private _preDestroyBinding(binding: Binding): Promise | void { + private _tryDeactivate(binding: Binding): Promise | void { if (!binding.cache) { return; } if (isPromise(binding.cache)) { - return binding.cache.then((resolved: any) => this._doDeactivation(binding, resolved)); + return binding.cache.then((resolved: any) => this._deactivate(binding, resolved)); } - return this._doDeactivation(binding, binding.cache); + return this._deactivate(binding, binding.cache); } - private _preDestroyBindings(bindings: Binding[]): void { + private _tryDeactivations(bindings: Binding[]): void { for (const binding of bindings) { - const result = this._preDestroyBinding(binding); + const result = this._tryDeactivate(binding); if (isPromise(result)) { throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); @@ -612,11 +609,11 @@ class Container implements interfaces.Container { } } - private async _preDestroyBindingsAsync(bindings: Binding[]): Promise { + private async _tryDeactivationsAsync(bindings: Binding[]): Promise { const promises: Promise[] = []; for (const binding of bindings) { - const result = this._preDestroyBinding(binding); + const result = this._tryDeactivate(binding); if (isPromise(result)) { promises.push(result); @@ -626,27 +623,27 @@ class Container implements interfaces.Container { await Promise.all(promises); } - private _propagateDeactivation( + private _propagateServiceDeactivationThenBindingAndPreDestroy( binding: Binding, instance: T, constructor: any ): void | Promise { if (this.parent) { - return this._doDeactivation.bind(this.parent)(binding, instance); + return this._deactivate.bind(this.parent)(binding, instance); } else { - return this._triggerOnDeactivationAndDestroyMetadata(binding, instance, constructor); + return this._bindingDeactivationAndPreDestroy(binding, instance, constructor); } } - private async _propagateDeactivationAsync( + private async _propagateServiceDeactivationThenBindingAndPreDestroyAsync( binding: Binding, instance: T, constructor: any ): Promise { if (this.parent) { - await this._doDeactivation.bind(this.parent)(binding, instance); + await this._deactivate.bind(this.parent)(binding, instance); } else { - await this._triggerOnDeactivationAndDestroyMetadataAsync(binding, instance, constructor); + await this._bindingDeactivationAndPreDestroyAsync(binding, instance, constructor); } } @@ -658,7 +655,7 @@ class Container implements interfaces.Container { } } - private _triggerOnDeactivationAndDestroyMetadata( + private _bindingDeactivationAndPreDestroy( binding: Binding, instance: T, constructor: any @@ -667,14 +664,14 @@ class Container implements interfaces.Container { const result = binding.onDeactivation(instance); if (isPromise(result)) { - return result.then(() => this._destroyMetadata(constructor, instance)); + return result.then(() => this._preDestroy(constructor, instance)); } } - return this._destroyMetadata(constructor, instance); + return this._preDestroy(constructor, instance); } - private async _triggerOnDeactivationAndDestroyMetadataAsync( + private async _bindingDeactivationAndPreDestroyAsync( binding: Binding, instance: T, constructor: any @@ -683,7 +680,7 @@ class Container implements interfaces.Container { await binding.onDeactivation(instance); } - await this._destroyMetadata(constructor, instance); + await this._preDestroy(constructor, instance); } } diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index ab8d6d532..f23b2b130 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -5,26 +5,6 @@ import { interfaces } from "../interfaces/interfaces"; import { Metadata } from "../planning/metadata"; import { isPromise } from "../utils/async"; -function _injectProperties( - instance: T, - childRequests: interfaces.Request[], - resolveRequest: interfaces.ResolveRequestHandler -): T { - const propertyInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ClassProperty); - - const propertyInjections = propertyInjectionsRequests.map(resolveRequest); - - propertyInjectionsRequests.forEach((r: interfaces.Request, index: number) => { - const propertyName = r.target.name.value(); - const injection = propertyInjections[index]; - - (instance as Record)[propertyName] = injection; - }); - - return instance; - -} - function _createInstance( constr: interfaces.Newable, childRequests: interfaces.Request[], @@ -33,17 +13,39 @@ function _createInstance( let result: T | Promise; if (childRequests.length > 0) { - const constructorInjections = _getConstructionInjections(childRequests, resolveRequest); - - if (constructorInjections.some(isPromise)) { - result = _createInstanceWithConstructorInjectionsAsync( - constructorInjections, - constr, - childRequests, - resolveRequest - ); - } else { - result = _createInstanceWithConstructorInjections(constructorInjections, constr,childRequests, resolveRequest); + let isAsync = false + const constructorInjections: unknown[] = [] + const propertyRequests: interfaces.Request[] = [] + const propertyInjections: unknown[] = [] + for(const childRequest of childRequests){ + let injection:unknown + const target = childRequest.target + const targetType = target.type + if(targetType === TargetTypeEnum.ConstructorArgument){ + injection = resolveRequest(childRequest) + constructorInjections.push(injection) + }else{ + propertyRequests.push(childRequest) + injection = resolveRequest(childRequest) + propertyInjections.push(injection) + } + if(!isAsync){ + if(Array.isArray(injection)){ + for(const arrayInjection of injection){ + if(isPromise(arrayInjection)){ + isAsync = true + break + } + } + }else if(isPromise(injection)){ + isAsync = true + } + } + } + if(isAsync){ + result = createInstanceWithInjectionsAsync(constructorInjections,propertyRequests,propertyInjections, constr) + }else{ + result = createInstanceWithInjections(constructorInjections,propertyRequests,propertyInjections, constr) } } else { result = new constr(); @@ -52,43 +54,39 @@ function _createInstance( return result; } -function _createInstanceWithConstructorInjections( - constructorInjections: unknown[], - constr: interfaces.Newable, - childRequests: interfaces.Request[], - resolveRequest: interfaces.ResolveRequestHandler, -): T { - let result: T; +function createInstanceWithInjections( + constructorArgs:unknown[], + propertyRequests:interfaces.Request[], + propertyValues:unknown[], + constr:interfaces.Newable): T{ + const instance = new constr(...constructorArgs); + propertyRequests.forEach((r: interfaces.Request, index: number) => { + const propertyName = r.target.name.value(); + const injection = propertyValues[index]; + (instance as Record)[propertyName] = injection; + }); + return instance - result = new constr(...constructorInjections); - result = _injectProperties(result, childRequests, resolveRequest); - - return result; -} - -async function _createInstanceWithConstructorInjectionsAsync( - constructorInjections: (unknown | Promise)[], - constr: interfaces.Newable, - childRequests: interfaces.Request[], - resolveRequest: interfaces.ResolveRequestHandler, -): Promise { - return _createInstanceWithConstructorInjections( - await Promise.all(constructorInjections), - constr, - childRequests, - resolveRequest, - ); } - -function _filterRequestsByTargetType(requests: interfaces.Request[], type: interfaces.TargetType): interfaces.Request[] { - return requests.filter((request: interfaces.Request) => - (request.target !== null && request.target.type === type)); +async function createInstanceWithInjectionsAsync( + possiblePromiseConstructorArgs:unknown[], + propertyRequests:interfaces.Request[], + possiblyPromisePropertyValues:unknown[], + constr:interfaces.Newable):Promise{ + const ctorArgs = await possiblyWaitInjections(possiblePromiseConstructorArgs) + const propertyValues = await possiblyWaitInjections(possiblyPromisePropertyValues) + return createInstanceWithInjections(ctorArgs,propertyRequests,propertyValues, constr) } - -function _getConstructionInjections(childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler): unknown[] { - const constructorInjectionsRequests = _filterRequestsByTargetType(childRequests, TargetTypeEnum.ConstructorArgument); - - return constructorInjectionsRequests.map(resolveRequest); +async function possiblyWaitInjections(possiblePromiseinjections:unknown[]){ + const injections:unknown[] = []; + for(const injection of possiblePromiseinjections){ + if(Array.isArray(injection)){ + injections.push(Promise.all(injection)) + }else{ + injections.push(injection) + } + } + return Promise.all(injections) } function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: T): T | Promise { diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index efa2a0ab3..0a22e6756 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -290,7 +290,7 @@ const _getParentContainersIterator = (container: interfaces.Container, includeSe return containersIterator; } -function resolve(context: interfaces.Context): T { +function resolve(context: interfaces.Context): (T | T[] | Promise | Promise[]) { const _f = _resolveRequest(context.plan.rootRequest.requestScope); return _f(context.plan.rootRequest); } diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index e5715a30e..897b7f083 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -18,6 +18,10 @@ import { getBindingDictionary, plan } from "../../src/planning/planner"; import { resolveInstance } from "../../src/resolution/instantiation"; import { resolve } from "../../src/resolution/resolver"; +function resolveTyped(context:interfaces.Context){ + return resolve(context) as T +} + describe("Resolve", () => { let sandbox: sinon.SinonSandbox; @@ -97,7 +101,7 @@ describe("Resolve", () => { container.bind(katanaHandlerId).to(KatanaHandler); const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -180,10 +184,10 @@ describe("Resolve", () => { expect(katanaBinding.cache === null).eql(true); expect(katanaBinding.activated).eql(false); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); - const ninja2 = resolve(context); + const ninja2 = resolveTyped(context); expect(ninja2 instanceof Ninja).eql(true); expect(katanaBinding.cache instanceof Katana).eql(true); @@ -230,7 +234,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); const throwFunction = () => { - resolve(context); + resolveTyped(context); }; expect(context.plan.rootRequest.bindings[0].type).eql(BindingTypeEnum.Invalid); @@ -302,7 +306,7 @@ describe("Resolve", () => { const katanaBinding = getBindingDictionary(container).get(katanaId)[0]; expect(katanaBinding.activated).eql(false); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(katanaBinding.activated).eql(true); @@ -415,7 +419,7 @@ describe("Resolve", () => { container.bind>(newableKatanaId).toConstructor(Katana); // IMPORTANT! const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -500,7 +504,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -581,7 +585,7 @@ describe("Resolve", () => { container.bind>(katanaFactoryId).toAutoFactory(katanaId); const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -672,7 +676,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.shuriken instanceof Shuriken).eql(true); @@ -724,7 +728,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -770,7 +774,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -820,7 +824,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -870,7 +874,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(ninja.katana instanceof Katana).eql(true); @@ -883,13 +887,108 @@ describe("Resolve", () => { const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); - const ninja2 = resolve(context2); + const ninja2 = resolveTyped(context2); expect(ninja2 instanceof Ninja).eql(true); expect(ninja2.katana instanceof Katana).eql(true); }); + it("Should be able to resolve plans with async multi-injections", async () => { + + interface Weapon { + name: string; + } + + @injectable() + class Katana implements Weapon { + public name = "Katana"; + } + + @injectable() + class Shuriken implements Weapon { + public name = "Shuriken"; + } + + interface Warrior { + katana: Weapon; + shuriken: Weapon; + } + + @injectable() + class Ninja implements Warrior { + public katana: Weapon; + public shuriken: Weapon; + public constructor( + @multiInject("Weapon") weapons: Weapon[] + ) { + this.katana = weapons[0]; + this.shuriken = weapons[1]; + } + } + + const ninjaId = "Ninja"; + const weaponId = "Weapon"; + + const container = new Container(); + container.bind(ninjaId).to(Ninja); + container.bind(weaponId).toDynamicValue(_ => Promise.resolve(new Katana())); + container.bind(weaponId).to(Shuriken); + + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); + + const ninja = await resolveTyped>(context); + + expect(ninja instanceof Ninja).eql(true); + expect(ninja.katana instanceof Katana).eql(true); + expect(ninja.shuriken instanceof Shuriken).eql(true); + + // if only one value is bound to weaponId + const container2 = new Container(); + container2.bind(ninjaId).to(Ninja); + container2.bind(weaponId).toDynamicValue(_ => new Katana()); + + const context2 = plan(new MetadataReader(), container2, false, TargetTypeEnum.Variable, ninjaId); + + const ninja2 = await resolveTyped>(context2); + + expect(ninja2 instanceof Ninja).eql(true); + expect(ninja2.katana instanceof Katana).eql(true); + expect(ninja2.shuriken === undefined).eql(true) + + }); + + it("Should be able to resolve plans with async and non async injections", async () => { + const syncPropertyId = "syncProperty" + const asyncPropertyId = "asyncProperty" + const syncCtorId = "syncCtor" + const asyncCtorId = "asyncCtor" + @injectable() + class CrazyInjectable{ + public constructor( + @inject(syncCtorId) readonly syncCtor:string, + @inject(asyncCtorId) readonly asyncCtor:string){} + @inject(syncPropertyId) + public syncProperty:string + @inject(asyncPropertyId) + public asyncProperty:string + } + const crazyInjectableId ='crazy' + const container = new Container(); + container.bind(crazyInjectableId).to(CrazyInjectable); + container.bind(syncCtorId).toConstantValue("syncCtor") + container.bind(asyncCtorId).toDynamicValue(_ => Promise.resolve('asyncCtor')) + container.bind(syncPropertyId).toConstantValue("syncProperty") + container.bind(asyncPropertyId).toDynamicValue(_ => Promise.resolve('asyncProperty')) + const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, crazyInjectableId); + const crazyInjectable = await resolveTyped>(context); + expect(crazyInjectable.syncCtor).eql("syncCtor") + expect(crazyInjectable.asyncCtor).eql("asyncCtor") + expect(crazyInjectable.syncProperty).eql("syncProperty") + expect(crazyInjectable.asyncProperty).eql("asyncProperty") + + }); + it("Should be able to resolve plans with activation handlers", () => { interface Sword { @@ -943,7 +1042,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja.katana.use()).eql("Used Katana!"); expect(Array.isArray(timeTracker)).eql(true); @@ -1018,7 +1117,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja instanceof Ninja).eql(true); expect(typeof ninja.katanaFactory === "function").eql(true); @@ -1073,7 +1172,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja.katana.use()).eql("Used Katana!"); @@ -1140,7 +1239,7 @@ describe("Resolve", () => { const context = plan(new MetadataReader(), container, false, TargetTypeEnum.Variable, ninjaId); - const ninja = resolve(context); + const ninja = resolveTyped(context); expect(ninja.katana.use()).eql("Used Weapon!"); From 8854c428ddd47d25670d423a75f17a2e88424642 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 22 Apr 2021 20:53:27 +0100 Subject: [PATCH 37/64] Container get all async methods to Promise. singular async to async functions --- src/container/container.ts | 23 +++++++++++++---------- src/interfaces/interfaces.ts | 6 +++--- test/container/container.test.ts | 13 +++++++------ 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index b817d4529..a7f20e73d 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -302,10 +302,10 @@ class Container implements interfaces.Container { return this._getButThrowIfAsync(getArgs) as T; } - public getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + public async getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, false); - return this._get(getArgs) as Promise; + return this._get(getArgs) as Promise|T; } public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T { @@ -314,10 +314,13 @@ class Container implements interfaces.Container { return this._getButThrowIfAsync(getArgs) as T; } - public getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise { - const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); + public async getTaggedAsync( + serviceIdentifier: interfaces.ServiceIdentifier, + key: string | number | symbol, + value: any): Promise { + const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); - return this._get(getArgs) as Promise; + return this._get(getArgs) as Promise|T; } public getNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T { @@ -336,10 +339,10 @@ class Container implements interfaces.Container { return this._getButThrowIfAsync(getArgs) as T[]; } - public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise[] { + public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { const getArgs = this._getAllArgs(serviceIdentifier); - return this._get(getArgs) as Promise[]; + return Promise.all(this._get(getArgs) as (Promise|T)[]); } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { @@ -352,17 +355,17 @@ class Container implements interfaces.Container { serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any - ): Promise[] { + ): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return this._get(getArgs) as Promise[]; + return Promise.all(this._get(getArgs) as (Promise|T)[]); } public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { return this.getAllTagged(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } - public getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise[] { + public getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise { return this.getAllTaggedAsync(serviceIdentifier, METADATA_KEY.NAMED_TAG, named); } diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index bfc9a6f73..a025cf876 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -190,9 +190,9 @@ namespace interfaces { getAsync(serviceIdentifier: ServiceIdentifier): Promise; getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise; getTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): Promise; - getAllAsync(serviceIdentifier: ServiceIdentifier): Promise[]; - getAllTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): Promise[]; - getAllNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise[]; + getAllAsync(serviceIdentifier: ServiceIdentifier): Promise; + getAllTaggedAsync(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): Promise; + getAllNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise; onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation): void; onDeactivation(serviceIdentifier: ServiceIdentifier, onDeactivation: BindingDeactivation): void; resolve(constructorFunction: interfaces.Newable): T; diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 679613d6f..2eb2fc5ac 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -909,12 +909,12 @@ describe("Container", () => { container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "hola" })).whenTargetNamed("es"); container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "adios" })).whenTargetNamed("es"); - const fr = await Promise.all(container.getAllNamedAsync("Intl", "fr")); + const fr = await container.getAllNamedAsync("Intl", "fr"); expect(fr.length).to.equal(2); expect(fr[0].hello).to.equal("bonjour"); expect(fr[1].goodbye).to.equal("au revoir"); - const es = await Promise.all(container.getAllNamedAsync("Intl", "es")); + const es = await container.getAllNamedAsync("Intl", "es"); expect(es.length).to.equal(2); expect(es[0].hello).to.equal("hola"); expect(es[1].goodbye).to.equal("adios"); @@ -951,12 +951,12 @@ describe("Container", () => { container.bind("Intl").toDynamicValue(() => Promise.resolve({ hello: "hola" })).whenTargetTagged("lang", "es"); container.bind("Intl").toDynamicValue(() => Promise.resolve({ goodbye: "adios" })).whenTargetTagged("lang", "es"); - const fr = await Promise.all(container.getAllTaggedAsync("Intl", "lang", "fr")); + const fr = await container.getAllTaggedAsync("Intl", "lang", "fr"); expect(fr.length).to.equal(2); expect(fr[0].hello).to.equal("bonjour"); expect(fr[1].goodbye).to.equal("au revoir"); - const es = await Promise.all(container.getAllTaggedAsync("Intl", "lang", "es")); + const es = await container.getAllTaggedAsync("Intl", "lang", "es"); expect(es.length).to.equal(2); expect(es[0].hello).to.equal("hola"); expect(es[1].goodbye).to.equal("adios"); @@ -985,12 +985,13 @@ describe("Container", () => { const firstValueBinded = "value-one"; const secondValueBinded = "value-two"; + const thirdValueBinded = "value-three"; container.bind(serviceIdentifier).toConstantValue(firstValueBinded); container.bind(serviceIdentifier).toConstantValue(secondValueBinded); - + container.bind(serviceIdentifier).toDynamicValue(_ => Promise.resolve(thirdValueBinded)); const services = await container.getAllAsync(serviceIdentifier); - expect(services).to.deep.eq([firstValueBinded, secondValueBinded]); + expect(services).to.deep.eq([firstValueBinded, secondValueBinded, thirdValueBinded]); }) }); From 62390ba451211273a6dfa4a28d1b845c28204272 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 22 Apr 2021 21:38:54 +0100 Subject: [PATCH 38/64] container resolution type correction and improve lazy in sync test --- src/container/container.ts | 26 +++++++++++++++----------- src/interfaces/interfaces.ts | 1 + src/resolution/instantiation.ts | 7 +------ 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index a7f20e73d..f0cb7b0bd 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -503,35 +503,39 @@ class Container implements interfaces.Container { // Prepares arguments required for resolution and // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve - private _get(getArgs: GetArgs): (T | T[] | Promise | Promise[]) { - let result: (T | T[] | Promise | Promise[]) | null = null; + private _get(getArgs: GetArgs): interfaces.ContainerResolution { const planAndResolveArgs:interfaces.NextArgs = { ...getArgs, contextInterceptor:(context) => context, targetType: TargetTypeEnum.Variable } if (this._middleware) { - result = this._middleware(planAndResolveArgs); - if (result === undefined || result === null) { + const middlewareResult = this._middleware(planAndResolveArgs); + if (middlewareResult === undefined || middlewareResult === null) { throw new Error(ERROR_MSGS.INVALID_MIDDLEWARE_RETURN); } - } else { - result = this._planAndResolve()(planAndResolveArgs); + return middlewareResult } - return result; + return this._planAndResolve()(planAndResolveArgs); } private _getButThrowIfAsync( getArgs: GetArgs, - ): (T | T[] | Promise | Promise[]) { + ): (T | T[]) { + let lazyInSyncError = false; const result = this._get(getArgs); + if (Array.isArray(result) && result.some(isPromise)) { + lazyInSyncError = true; + } else if (isPromise(result)){ + lazyInSyncError = true; + } - if (isPromise(result)) { + if (lazyInSyncError) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); } - return result; + return result as (T | T[]); } private _getAllArgs(serviceIdentifier: interfaces.ServiceIdentifier): GetArgs { @@ -564,7 +568,7 @@ class Container implements interfaces.Container { // Planner creates a plan and Resolver resolves a plan // one of the jobs of the Container is to links the Planner // with the Resolver and that is what this function is about - private _planAndResolve(): (args: interfaces.NextArgs) => (T | T[] | Promise | Promise[]) { + private _planAndResolve(): (args: interfaces.NextArgs) => interfaces.ContainerResolution { return (args: interfaces.NextArgs) => { // create a plan diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index a025cf876..e8fd2a575 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -1,4 +1,5 @@ namespace interfaces { + export type ContainerResolution = T | Promise | (T | Promise)[] type AsyncCallback = TCallback extends (...args: infer TArgs) => infer TResult ? (...args: TArgs) => Promise diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index f23b2b130..42948d1b2 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -31,12 +31,7 @@ function _createInstance( } if(!isAsync){ if(Array.isArray(injection)){ - for(const arrayInjection of injection){ - if(isPromise(arrayInjection)){ - isAsync = true - break - } - } + isAsync = injection.some(isPromise) }else if(isPromise(injection)){ isAsync = true } From 1cc0df4f0afb0152eb890cbceffe56e36e48253f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Thu, 22 Apr 2021 23:42:50 +0200 Subject: [PATCH 39/64] add typings at resolver and compact boolean logic at container._getButThrowIfAsync --- src/container/container.ts | 9 +++------ src/resolution/resolver.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index f0cb7b0bd..ef4b0a9ae 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -523,13 +523,10 @@ class Container implements interfaces.Container { private _getButThrowIfAsync( getArgs: GetArgs, ): (T | T[]) { - let lazyInSyncError = false; const result = this._get(getArgs); - if (Array.isArray(result) && result.some(isPromise)) { - lazyInSyncError = true; - } else if (isPromise(result)){ - lazyInSyncError = true; - } + + const isArrayContainingPromise = Array.isArray(result) && result.some(isPromise); + const lazyInSyncError = isArrayContainingPromise || isPromise(result); if (lazyInSyncError) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 0a22e6756..65c4017a2 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -27,8 +27,8 @@ const invokeFactory = ( } }; -const _resolveRequest = (requestScope: interfaces.RequestScope) => - (request: interfaces.Request): any => { +const _resolveRequest = (requestScope: interfaces.RequestScope) => + (request: interfaces.Request): undefined | T | Promise | (T | Promise)[] => { request.parentContext.setCurrentRequest(request); @@ -47,12 +47,12 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => // Create an array instead of creating an instance return childRequests.map((childRequest: interfaces.Request) => { const _f = _resolveRequest(requestScope); - return _f(childRequest); + return _f(childRequest) as T | Promise; }); } else { - let result: any = null; + let result: undefined | T | Promise | (T | Promise)[]; if (request.target.isOptional() && bindings.length === 0) { return undefined; @@ -81,7 +81,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => result = binding.cache; binding.activated = true; } else if (binding.type === BindingTypeEnum.Constructor) { - result = binding.implementationType; + result = binding.implementationType as unknown as T; } else if (binding.type === BindingTypeEnum.DynamicValue && binding.dynamicValue !== null) { result = invokeFactory( "toDynamicValue", @@ -290,9 +290,9 @@ const _getParentContainersIterator = (container: interfaces.Container, includeSe return containersIterator; } -function resolve(context: interfaces.Context): (T | T[] | Promise | Promise[]) { +function resolve(context: interfaces.Context): T | Promise | (T | Promise)[] { const _f = _resolveRequest(context.plan.rootRequest.requestScope); - return _f(context.plan.rootRequest); + return _f(context.plan.rootRequest) as T | Promise | (T | Promise)[]; } export { resolve }; From 53a739cd5eb7a01600abf63b6826de0b80bf4e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Thu, 22 Apr 2021 23:56:52 +0200 Subject: [PATCH 40/64] add isPromiseOrContainsPromise --- src/container/container.ts | 7 ++----- src/resolution/instantiation.ts | 8 ++------ src/utils/async.ts | 10 +++++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index ef4b0a9ae..487348861 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -7,7 +7,7 @@ import { MetadataReader } from "../planning/metadata_reader"; import { createMockRequest, getBindingDictionary, plan } from "../planning/planner"; import { resolve } from "../resolution/resolver"; import { BindingToSyntax } from "../syntax/binding_to_syntax"; -import { isPromise } from "../utils/async"; +import { isPromise, isPromiseOrContainsPromise } from "../utils/async"; import { id } from "../utils/id"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; @@ -525,10 +525,7 @@ class Container implements interfaces.Container { ): (T | T[]) { const result = this._get(getArgs); - const isArrayContainingPromise = Array.isArray(result) && result.some(isPromise); - const lazyInSyncError = isArrayContainingPromise || isPromise(result); - - if (lazyInSyncError) { + if (isPromiseOrContainsPromise(result)) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); } diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 42948d1b2..c7be51e48 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -3,7 +3,7 @@ import { TargetTypeEnum } from "../constants/literal_types"; import * as METADATA_KEY from "../constants/metadata_keys"; import { interfaces } from "../interfaces/interfaces"; import { Metadata } from "../planning/metadata"; -import { isPromise } from "../utils/async"; +import { isPromise, isPromiseOrContainsPromise } from "../utils/async"; function _createInstance( constr: interfaces.Newable, @@ -30,11 +30,7 @@ function _createInstance( propertyInjections.push(injection) } if(!isAsync){ - if(Array.isArray(injection)){ - isAsync = injection.some(isPromise) - }else if(isPromise(injection)){ - isAsync = true - } + isAsync = isPromiseOrContainsPromise(injection); } } if(isAsync){ diff --git a/src/utils/async.ts b/src/utils/async.ts index 0bc635d21..f72d6735d 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -2,4 +2,12 @@ function isPromise(object: any): object is Promise { return object && object.then !== undefined && typeof object.then === "function"; } -export { isPromise }; +function isPromiseOrContainsPromise(object: unknown): object is Promise | (T | Promise)[] { + if (isPromise(object)) { + return true; + } + + return Array.isArray(object) && object.some(isPromise); +} + +export { isPromise, isPromiseOrContainsPromise }; From b5421a01ecab08bc3056765d2fdaad8b93af2e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Fri, 23 Apr 2021 00:18:15 +0200 Subject: [PATCH 41/64] update isPromise to receive unknown and a generic type --- src/resolution/resolver.ts | 2 +- src/utils/async.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 65c4017a2..8611d0821 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -197,7 +197,7 @@ const _callActivations = ( while (!activation.done) { result = activation.value(context, result); - if (isPromise(result)) { + if (isPromise(result)) { return result.then((resolved) => _callActivationsAsync(activationsIterator, context, resolved)); } diff --git a/src/utils/async.ts b/src/utils/async.ts index f72d6735d..17195235f 100644 --- a/src/utils/async.ts +++ b/src/utils/async.ts @@ -1,5 +1,8 @@ -function isPromise(object: any): object is Promise { - return object && object.then !== undefined && typeof object.then === "function"; +function isPromise(object: unknown): object is Promise { + // https://promisesaplus.com/ + const isObjectOrFunction = (typeof object === 'object' && object !== null) || typeof object === 'function'; + + return isObjectOrFunction && typeof (object as PromiseLike).then === "function"; } function isPromiseOrContainsPromise(object: unknown): object is Promise | (T | Promise)[] { From b7a1845e068964fab07ad4db56c3a132bc309f11 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 08:44:34 +0100 Subject: [PATCH 42/64] refactor _createInstance --- src/resolution/instantiation.ts | 100 ++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index c7be51e48..36e1bd7fd 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -5,6 +5,43 @@ import { interfaces } from "../interfaces/interfaces"; import { Metadata } from "../planning/metadata"; import { isPromise, isPromiseOrContainsPromise } from "../utils/async"; +interface InstanceCreationInstruction{ + constructorInjections: unknown[], + propertyInjections: unknown[], + propertyRequests:interfaces.Request[] +} + +type CreateInstanceWithInjectionArg = InstanceCreationInstruction & {constr: interfaces.Newable,} + +function _resolveRequests( + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler +) : InstanceCreationInstruction & { + isAsync:boolean +} { + let isAsync = false + const constructorInjections: unknown[] = [] + const propertyRequests: interfaces.Request[] = [] + const propertyInjections: unknown[] = [] + for(const childRequest of childRequests){ + let injection:unknown + const target = childRequest.target + const targetType = target.type + if(targetType === TargetTypeEnum.ConstructorArgument){ + injection = resolveRequest(childRequest) + constructorInjections.push(injection) + }else{ + propertyRequests.push(childRequest) + injection = resolveRequest(childRequest) + propertyInjections.push(injection) + } + if(!isAsync){ + isAsync = isPromiseOrContainsPromise(injection); + } + } + return {constructorInjections,propertyInjections,propertyRequests,isAsync} +} + function _createInstance( constr: interfaces.Newable, childRequests: interfaces.Request[], @@ -13,30 +50,12 @@ function _createInstance( let result: T | Promise; if (childRequests.length > 0) { - let isAsync = false - const constructorInjections: unknown[] = [] - const propertyRequests: interfaces.Request[] = [] - const propertyInjections: unknown[] = [] - for(const childRequest of childRequests){ - let injection:unknown - const target = childRequest.target - const targetType = target.type - if(targetType === TargetTypeEnum.ConstructorArgument){ - injection = resolveRequest(childRequest) - constructorInjections.push(injection) - }else{ - propertyRequests.push(childRequest) - injection = resolveRequest(childRequest) - propertyInjections.push(injection) - } - if(!isAsync){ - isAsync = isPromiseOrContainsPromise(injection); - } - } - if(isAsync){ - result = createInstanceWithInjectionsAsync(constructorInjections,propertyRequests,propertyInjections, constr) + const resolved = _resolveRequests(childRequests,resolveRequest) + const createInstanceWithInjectionsArg: CreateInstanceWithInjectionArg = {...resolved,constr} + if(resolved.isAsync){ + result = createInstanceWithInjectionsAsync(createInstanceWithInjectionsArg) }else{ - result = createInstanceWithInjections(constructorInjections,propertyRequests,propertyInjections, constr) + result = createInstanceWithInjections(createInstanceWithInjectionsArg) } } else { result = new constr(); @@ -46,28 +65,25 @@ function _createInstance( } function createInstanceWithInjections( - constructorArgs:unknown[], - propertyRequests:interfaces.Request[], - propertyValues:unknown[], - constr:interfaces.Newable): T{ - const instance = new constr(...constructorArgs); - propertyRequests.forEach((r: interfaces.Request, index: number) => { - const propertyName = r.target.name.value(); - const injection = propertyValues[index]; - (instance as Record)[propertyName] = injection; - }); - return instance - + args:CreateInstanceWithInjectionArg +): T { + const instance = new args.constr(...args.constructorInjections); + args.propertyRequests.forEach((r: interfaces.Request, index: number) => { + const propertyName = r.target.name.value(); + const injection = args.propertyInjections[index]; + (instance as Record)[propertyName] = injection; + }); + return instance } + async function createInstanceWithInjectionsAsync( - possiblePromiseConstructorArgs:unknown[], - propertyRequests:interfaces.Request[], - possiblyPromisePropertyValues:unknown[], - constr:interfaces.Newable):Promise{ - const ctorArgs = await possiblyWaitInjections(possiblePromiseConstructorArgs) - const propertyValues = await possiblyWaitInjections(possiblyPromisePropertyValues) - return createInstanceWithInjections(ctorArgs,propertyRequests,propertyValues, constr) + args:CreateInstanceWithInjectionArg +): Promise { + const constructorInjections = await possiblyWaitInjections(args.constructorInjections) + const propertyInjections = await possiblyWaitInjections(args.propertyInjections) + return createInstanceWithInjections({...args,constructorInjections,propertyInjections}) } + async function possiblyWaitInjections(possiblePromiseinjections:unknown[]){ const injections:unknown[] = []; for(const injection of possiblePromiseinjections){ From 74e07a497a6efec012660e177d65424cc573d2f6 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 09:56:44 +0100 Subject: [PATCH 43/64] refactor _resolveRequests with array reduce --- src/resolution/instantiation.ts | 35 +++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 36e1bd7fd..e0f2100c9 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -11,35 +11,28 @@ interface InstanceCreationInstruction{ propertyRequests:interfaces.Request[] } -type CreateInstanceWithInjectionArg = InstanceCreationInstruction & {constr: interfaces.Newable,} +type ResolvedRequests = InstanceCreationInstruction & {isAsync:boolean} + +type CreateInstanceWithInjectionArg = InstanceCreationInstruction & {constr: interfaces.Newable} function _resolveRequests( childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler -) : InstanceCreationInstruction & { - isAsync:boolean -} { - let isAsync = false - const constructorInjections: unknown[] = [] - const propertyRequests: interfaces.Request[] = [] - const propertyInjections: unknown[] = [] - for(const childRequest of childRequests){ - let injection:unknown - const target = childRequest.target - const targetType = target.type +) : ResolvedRequests { + return childRequests.reduce((resolvedRequests,childRequest)=> { + const injection = resolveRequest(childRequest) + const targetType = childRequest.target.type if(targetType === TargetTypeEnum.ConstructorArgument){ - injection = resolveRequest(childRequest) - constructorInjections.push(injection) + resolvedRequests.constructorInjections.push(injection) }else{ - propertyRequests.push(childRequest) - injection = resolveRequest(childRequest) - propertyInjections.push(injection) + resolvedRequests.propertyRequests.push(childRequest) + resolvedRequests.propertyInjections.push(injection) } - if(!isAsync){ - isAsync = isPromiseOrContainsPromise(injection); + if(!resolvedRequests.isAsync){ + resolvedRequests.isAsync = isPromiseOrContainsPromise(injection); } - } - return {constructorInjections,propertyInjections,propertyRequests,isAsync} + return resolvedRequests + },{constructorInjections:[],propertyInjections:[],propertyRequests:[],isAsync:false}) } function _createInstance( From c7f97df9aa0c4fb5c90598889c978e3bfa178276 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 10:52:01 +0100 Subject: [PATCH 44/64] type to interface --- src/resolution/instantiation.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index e0f2100c9..6c3b41a1d 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -11,9 +11,13 @@ interface InstanceCreationInstruction{ propertyRequests:interfaces.Request[] } -type ResolvedRequests = InstanceCreationInstruction & {isAsync:boolean} +interface ResolvedRequests extends InstanceCreationInstruction{ + isAsync:boolean +} -type CreateInstanceWithInjectionArg = InstanceCreationInstruction & {constr: interfaces.Newable} +interface CreateInstanceWithInjectionArg extends InstanceCreationInstruction{ + constr: interfaces.Newable +} function _resolveRequests( childRequests: interfaces.Request[], From ad07f4bf9aec9e1468770bdbe255758ef7ce6617 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 11:56:13 +0100 Subject: [PATCH 45/64] test coverage --- src/resolution/resolver.ts | 10 ++---- test/container/lookup.test.ts | 18 ++++++++++ test/resolution/resolver.test.ts | 59 +++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 8611d0821..6fec3fcf9 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -152,7 +152,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => function _onActivation(request: interfaces.Request, binding: interfaces.Binding, previousResult: T): T | Promise { let result = _callBindingActivation(request, binding, previousResult); - const containersIterator = _getParentContainersIterator(request.parentContext.container, true); + const containersIterator = _getContainersIterator(request.parentContext.container); let container: interfaces.Container; let containersIteratorResult = containersIterator.next(); @@ -258,12 +258,8 @@ const _extractActivationsForService = (container: interfaces.Container, servi return activations.hasKey(serviceIdentifier) ? activations.get(serviceIdentifier).values() : [].values(); } -const _getParentContainersIterator = (container: interfaces.Container, includeSelf: boolean = false): Iterator => { - const containersStack: interfaces.Container[] = []; - - if (includeSelf) { - containersStack.push(container); - } +const _getContainersIterator = (container: interfaces.Container): Iterator => { + const containersStack: interfaces.Container[] = [container]; let parent = container.parent; diff --git a/test/container/lookup.test.ts b/test/container/lookup.test.ts index 7d1d4a7f7..4fc6cb1f2 100644 --- a/test/container/lookup.test.ts +++ b/test/container/lookup.test.ts @@ -86,6 +86,24 @@ describe("Lookup", () => { }); + it("Should use use the original non clonable entry if it is not clonable", () => { + const lookup = new Lookup(); + const key1 = Symbol.for("TEST_KEY"); + + class Warrior { + public kind: string; + public constructor(kind: string) { + this.kind = kind; + } + } + const warrior = new Warrior("ninja") + lookup.add(key1, warrior); + + const copy = lookup.clone(); + expect(copy.get(key1)[0] === warrior).to.eql(true); + + }) + it("Should be able to remove a binding by a condition", () => { const moduleId1 = "moduleId1"; diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index 897b7f083..f6070de1c 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1364,15 +1364,33 @@ describe("Resolve", () => { } const container = new Container(); + let deactivatedDestroyable:Destroyable|null = null container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() .onDeactivation((instance) => new Promise((r) => { - expect(instance).instanceof(Destroyable); + deactivatedDestroyable = instance r(); })); await container.getAsync("Destroyable"); await container.unbindAsync("Destroyable"); + + expect(deactivatedDestroyable).instanceof(Destroyable); + + // with BindingInWhenOnSyntax + const container2 = new Container({defaultScope: "Singleton"}); + let deactivatedDestroyable2:Destroyable|null = null + container2.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())) + .onDeactivation((instance) => new Promise((r) => { + deactivatedDestroyable2 = instance + r(); + })); + + await container2.getAsync("Destroyable"); + + await container2.unbindAsync("Destroyable"); + + expect(deactivatedDestroyable2).instanceof(Destroyable); }); it("Should wait on deactivation promise before returning unbindAsync()", async () => { @@ -1546,6 +1564,45 @@ describe("Resolve", () => { .throw("Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"); }); + it("Should throw deactivation error when errors in deactivation ( sync )", () => { + @injectable() + class Destroyable { + } + const errorMessage = "the error message" + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => {throw new Error(errorMessage)}); + + container.get("Destroyable"); + + const expectedErrorMessage = ERROR_MSGS.ON_DEACTIVATION_ERROR("Destroyable",errorMessage) + + expect(() => container.unbind("Destroyable")).to + .throw(expectedErrorMessage); + }) + + it("Should throw deactivation error when errors in deactivation ( async )", async () => { + @injectable() + class Destroyable { + } + const errorMessage = "the error message" + const container = new Container(); + container.bind("Destroyable").to(Destroyable).inSingletonScope() + .onDeactivation(() => Promise.reject(new Error(errorMessage))); + + container.get("Destroyable"); + + const expectedErrorMessage = ERROR_MSGS.ON_DEACTIVATION_ERROR("Destroyable",errorMessage) + + let error:any + try{ + await container.unbindAsync("Destroyable") + }catch(e){ + error = e + } + expect(error.message).to.eql(expectedErrorMessage) + }) + it("Should invoke destroy in order (all async): child container, parent container, binding, class", async () => { let roll = 1; let binding = null; From 6e58b9a2add7c976585b81a04570116fbeda6540 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 12:07:14 +0100 Subject: [PATCH 46/64] correct spelling in tests and correct expect in handler --- test/container/container.test.ts | 2 +- test/resolution/resolver.test.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 2eb2fc5ac..5368e7d61 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -978,7 +978,7 @@ describe("Container", () => { }); - it("should be able to get all the servides binded (async)", async () => { + it("should be able to get all the services binded (async)", async () => { const serviceIdentifier = "service-identifier"; const container = new Container(); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index f6070de1c..a59b397ef 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1655,7 +1655,7 @@ describe("Resolve", () => { expect(klass).eql(4); }); - it("Should invoke destory in order (sync + async): child container, parent container, binding, class", async () => { + it("Should invoke destroy in order (sync + async): child container, parent container, binding, class", async () => { let roll = 1; let binding = null; let klass = null; @@ -1703,7 +1703,7 @@ describe("Resolve", () => { expect(klass).eql(4); }); - it("Should invoke destory in order (all sync): child container, parent container, binding, class", () => { + it("Should invoke destroy in order (all sync): child container, parent container, binding, class", () => { let roll = 1; let binding = null; let klass = null; @@ -1745,7 +1745,7 @@ describe("Resolve", () => { expect(klass).eql(4); }); - it("Should invoke destory in order (async): child container, parent container, binding, class", async () => { + it("Should invoke destroy in order (async): child container, parent container, binding, class", async () => { let roll = 1; let binding = null; let klass = null; @@ -1865,16 +1865,16 @@ describe("Resolve", () => { @injectable() class Constructable { } - + let activated: Constructable | null = null const container = new Container(); container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() .onActivation((context, c) => new Promise((r) => { - expect(c).instanceof(Constructable); - + activated = c r(c); })); await container.getAsync("Constructable"); + expect(activated).instanceof(Constructable); }); it("Should not allow sync get if an async activation was added to container", async () => { @@ -2466,7 +2466,7 @@ describe("Resolve", () => { await container.getAsync(Child); }); - it("Should be able to mix BindingType.AsyncValue bindings with non-async values", async () => { + it("Should be able to mix async bindings with non-async values", async () => { @injectable() class UseDate implements UseDate { public currentDate: Date; From 455a48d1b36030e53dff036de57f07387d620073 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 16:46:31 +0100 Subject: [PATCH 47/64] deactivate refactor and name changes --- src/container/container.ts | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 487348861..64710fdcb 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -178,7 +178,7 @@ class Container implements interfaces.Container { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - this._tryDeactivations(bindings); + this._deactivateSingletons(bindings); } this._removeServiceFromDictionary(serviceIdentifier); @@ -188,7 +188,7 @@ class Container implements interfaces.Container { if (this._bindingDictionary.hasKey(serviceIdentifier)) { const bindings = this._bindingDictionary.get(serviceIdentifier); - await this._tryDeactivationsAsync(bindings); + await this._deactivateSingletonsAsync(bindings); } this._removeServiceFromDictionary(serviceIdentifier); @@ -197,7 +197,7 @@ class Container implements interfaces.Container { // Removes all the type bindings from the registry public unbindAll(): void { this._bindingDictionary.traverse((key, value) => { - this._tryDeactivations(value); + this._deactivateSingletons(value); }); this._bindingDictionary = new Lookup>(); @@ -207,7 +207,7 @@ class Container implements interfaces.Container { const promises: Promise[] = []; this._bindingDictionary.traverse((key, value) => { - promises.push(this._tryDeactivationsAsync(value)); + promises.push(this._deactivateSingletonsAsync(value)); }); await Promise.all(promises); @@ -588,7 +588,7 @@ class Container implements interfaces.Container { }; } - private _tryDeactivate(binding: Binding): Promise | void { + private _deactivateIfSingleton(binding: Binding): Promise | void { if (!binding.cache) { return; } @@ -600,9 +600,9 @@ class Container implements interfaces.Container { return this._deactivate(binding, binding.cache); } - private _tryDeactivations(bindings: Binding[]): void { + private _deactivateSingletons(bindings: Binding[]): void { for (const binding of bindings) { - const result = this._tryDeactivate(binding); + const result = this._deactivateIfSingleton(binding); if (isPromise(result)) { throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); @@ -610,18 +610,8 @@ class Container implements interfaces.Container { } } - private async _tryDeactivationsAsync(bindings: Binding[]): Promise { - const promises: Promise[] = []; - - for (const binding of bindings) { - const result = this._tryDeactivate(binding); - - if (isPromise(result)) { - promises.push(result); - } - } - - await Promise.all(promises); + private async _deactivateSingletonsAsync(bindings: Binding[]): Promise { + await Promise.all(bindings.map(b => this._deactivateIfSingleton(b))) } private _propagateServiceDeactivationThenBindingAndPreDestroy( From 4df7e91676c75eec00ccb1d0488160b7dcddaa45 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Fri, 23 Apr 2021 17:46:14 +0100 Subject: [PATCH 48/64] refactor activation and deactivation --- src/container/container.ts | 18 +++++++------ src/resolution/resolver.ts | 54 +++++++++++--------------------------- 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 64710fdcb..39a0746c1 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -392,20 +392,22 @@ class Container implements interfaces.Container { try { if (this._deactivations.hasKey(binding.serviceIdentifier)) { - const result = this._deactivateService( + const result = this._deactivateContainer( instance, this._deactivations.get(binding.serviceIdentifier).values(), ); if (isPromise(result)) { return this._handleDeactivationError( - result.then(() => this._propagateServiceDeactivationThenBindingAndPreDestroyAsync(binding, instance, constructor)), + result.then(() => this._propagateContainerDeactivationThenBindingAndPreDestroyAsync( + binding, instance, constructor)), constructor ); } } - const propagateDeactivationResult = this._propagateServiceDeactivationThenBindingAndPreDestroy(binding, instance, constructor); + const propagateDeactivationResult = this._propagateContainerDeactivationThenBindingAndPreDestroy( + binding, instance, constructor); if (isPromise(propagateDeactivationResult)) { return this._handleDeactivationError(propagateDeactivationResult, constructor); @@ -424,7 +426,7 @@ class Container implements interfaces.Container { } - private _deactivateService( + private _deactivateContainer( instance: T, deactivationsIterator: IterableIterator>, ): void | Promise { @@ -435,7 +437,7 @@ class Container implements interfaces.Container { if (isPromise(result)) { return result.then(() => - this._deactivateServiceAsync(instance, deactivationsIterator), + this._deactivateContainerAsync(instance, deactivationsIterator), ); } @@ -443,7 +445,7 @@ class Container implements interfaces.Container { } } - private async _deactivateServiceAsync( + private async _deactivateContainerAsync( instance: T, deactivationsIterator: IterableIterator>, ): Promise { @@ -614,7 +616,7 @@ class Container implements interfaces.Container { await Promise.all(bindings.map(b => this._deactivateIfSingleton(b))) } - private _propagateServiceDeactivationThenBindingAndPreDestroy( + private _propagateContainerDeactivationThenBindingAndPreDestroy( binding: Binding, instance: T, constructor: any @@ -626,7 +628,7 @@ class Container implements interfaces.Container { } } - private async _propagateServiceDeactivationThenBindingAndPreDestroyAsync( + private async _propagateContainerDeactivationThenBindingAndPreDestroyAsync( binding: Binding, instance: T, constructor: any diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 6fec3fcf9..ec81241ef 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -149,8 +149,8 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => }; -function _onActivation(request: interfaces.Request, binding: interfaces.Binding, previousResult: T): T | Promise { - let result = _callBindingActivation(request, binding, previousResult); +function _onActivation(request: interfaces.Request, binding: interfaces.Binding, resolved: T): T | Promise { + let result = _bindingActivation(request.parentContext, binding, resolved); const containersIterator = _getContainersIterator(request.parentContext.container); @@ -159,11 +159,14 @@ function _onActivation(request: interfaces.Request, binding: interfaces.Bindi do { container = containersIteratorResult.value; + const context = request.parentContext; + const serviceIdentifier = request.serviceIdentifier; + const activationsIterator = _getContainerActivationsForService(container, serviceIdentifier); if (isPromise(result)) { - result = _callContainerActivationsAsync(request, container, result); + result = _activateContainerAsync(activationsIterator, context, result); } else { - result = _callContainerActivations(request, container, result); + result = _activateContainer(activationsIterator, context, result); } containersIteratorResult = containersIterator.next(); @@ -174,12 +177,12 @@ function _onActivation(request: interfaces.Request, binding: interfaces.Bindi return result; } -const _callBindingActivation = (request: interfaces.Request, binding: interfaces.Binding, previousResult: T): T | Promise => { +const _bindingActivation = (context: interfaces.Context, binding: interfaces.Binding, previousResult: T): T | Promise => { let result: T | Promise; // use activation handler if available if (typeof binding.onActivation === "function") { - result = binding.onActivation(request.parentContext, previousResult); + result = binding.onActivation(context, previousResult); } else { result = previousResult; } @@ -187,7 +190,7 @@ const _callBindingActivation = (request: interfaces.Request, binding: interfa return result; } -const _callActivations = ( +const _activateContainer = ( activationsIterator: Iterator>, context: interfaces.Context, result: T, @@ -198,7 +201,7 @@ const _callActivations = ( result = activation.value(context, result); if (isPromise(result)) { - return result.then((resolved) => _callActivationsAsync(activationsIterator, context, resolved)); + return _activateContainerAsync(activationsIterator, context, result); } activation = activationsIterator.next(); @@ -207,11 +210,12 @@ const _callActivations = ( return result; } -const _callActivationsAsync = async( +const _activateContainerAsync = async( activationsIterator: Iterator>, context: interfaces.Context, - result: T, + resultPromise: Promise, ): Promise => { + let result = await resultPromise let activation = activationsIterator.next(); while (!activation.done) { @@ -223,35 +227,7 @@ const _callActivationsAsync = async( return result; } -const _callContainerActivations = ( - request: interfaces.Request, - container: interfaces.Container, - previousResult: T, -): T | Promise => { - const context = request.parentContext; - const serviceIdentifier = request.serviceIdentifier; - - const activationsIterator = _extractActivationsForService(container, serviceIdentifier); - - const activationsTraverseResult = _callActivations(activationsIterator, context, previousResult); - - return activationsTraverseResult; -} - -const _callContainerActivationsAsync = async ( - request: interfaces.Request, - container: interfaces.Container, - previousResult: T | Promise, -): Promise => { - const context = request.parentContext; - const serviceIdentifier = request.serviceIdentifier; - - const activationsIterator = _extractActivationsForService(container, serviceIdentifier); - - return _callActivationsAsync(activationsIterator, context, await previousResult); -} - -const _extractActivationsForService = (container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier) => { +const _getContainerActivationsForService = (container: interfaces.Container, serviceIdentifier: interfaces.ServiceIdentifier) => { // smell accessing _activations, but similar pattern is done in planner.getBindingDictionary() const activations = (container as any)._activations as interfaces.Lookup>; From e7c79435616da479f689b492f5744548af761e89 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Sat, 24 Apr 2021 20:15:47 +0100 Subject: [PATCH 49/64] update wikis and dynamic value typing --- src/bindings/binding.ts | 2 +- src/interfaces/interfaces.ts | 5 +++-- src/syntax/binding_to_syntax.ts | 2 +- wiki/activation_handler.md | 8 ++++++-- wiki/deactivation_handler.md | 6 ++++-- wiki/environment.md | 8 ++++++-- wiki/post_construct.md | 2 ++ wiki/value_injection.md | 2 ++ 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/bindings/binding.ts b/src/bindings/binding.ts index db1849564..b709c5207 100644 --- a/src/bindings/binding.ts +++ b/src/bindings/binding.ts @@ -22,7 +22,7 @@ class Binding implements interfaces.Binding { public cache: T | null; // Cache used to allow BindingType.DynamicValue bindings - public dynamicValue: ((context: interfaces.Context) => T) | null; + public dynamicValue: interfaces.DynamicValue | null; // The scope mode to be used public scope: interfaces.BindingScope; diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index e8fd2a575..98572f789 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -1,4 +1,5 @@ namespace interfaces { + export type DynamicValue = (context: interfaces.Context) => T | Promise export type ContainerResolution = T | Promise | (T | Promise)[] type AsyncCallback = @@ -57,7 +58,7 @@ namespace interfaces { activated: boolean; serviceIdentifier: ServiceIdentifier; constraint: ConstraintFunction; - dynamicValue: ((context: interfaces.Context) => T) | null; + dynamicValue: DynamicValue | null; scope: BindingScope; type: BindingType; implementationType: Newable | null; @@ -291,7 +292,7 @@ namespace interfaces { to(constructor: new (...args: any[]) => T): BindingInWhenOnSyntax; toSelf(): BindingInWhenOnSyntax; toConstantValue(value: T): BindingWhenOnSyntax; - toDynamicValue(func: (context: Context) => T | Promise): BindingInWhenOnSyntax; + toDynamicValue(func: DynamicValue): BindingInWhenOnSyntax; toConstructor(constructor: Newable): BindingWhenOnSyntax; toFactory(factory: FactoryCreator): BindingWhenOnSyntax; toFunction(func: T): BindingWhenOnSyntax; diff --git a/src/syntax/binding_to_syntax.ts b/src/syntax/binding_to_syntax.ts index 2b9163ff3..3dc508783 100644 --- a/src/syntax/binding_to_syntax.ts +++ b/src/syntax/binding_to_syntax.ts @@ -35,7 +35,7 @@ class BindingToSyntax implements interfaces.BindingToSyntax { return new BindingWhenOnSyntax(this._binding); } - public toDynamicValue(func: (context: interfaces.Context) => T): interfaces.BindingInWhenOnSyntax { + public toDynamicValue(func: interfaces.DynamicValue): interfaces.BindingInWhenOnSyntax { this._binding.type = BindingTypeEnum.DynamicValue; this._binding.cache = null; this._binding.dynamicValue = func; diff --git a/wiki/activation_handler.md b/wiki/activation_handler.md index c6c7b0cf1..9e03918ce 100644 --- a/wiki/activation_handler.md +++ b/wiki/activation_handler.md @@ -1,6 +1,10 @@ # Activation handler -It is possible to add an activation handler for a type. The activation handler is invoked after a dependency has been resolved and before it is added to the cache (if singleton) and injected. This is useful to keep our dependencies agnostic of the implementation of crosscutting concerns like caching or logging. The following example uses a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to intercept one of the methods (`use`) of a dependency (`Katana`). +It is possible to add an activation handler for a type. The activation handler is invoked after a dependency has been resolved and before it is added to a cache (if singleton or request singleton [see scope](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md)) and injected. The activation handler will not be invoked if type is resolved from a cache. The activation handler can be synchronous or asynchronous. + +Activation handlers are useful to keep our dependencies agnostic of the implementation of crosscutting concerns like caching or logging. + +The following example uses a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) to intercept one of the methods (`use`) of a dependency (`Katana`). ```ts interface Katana { @@ -57,4 +61,4 @@ There are multiple ways to provide an activation handler - Adding the handler to the container - Adding the handler to the binding -When multiple activation handlers are binded to a service identifier, the bindind handlers are called before any others. Any handler defined in a container is called before a handler defined in it's parent container +When multiple activation handlers are binded to a service identifier, the binding handler is called before any others. Then the container handlers are called, starting at the root container and descending the descendant containers stopping at the container with the binding. diff --git a/wiki/deactivation_handler.md b/wiki/deactivation_handler.md index d1ee194d6..62759205c 100644 --- a/wiki/deactivation_handler.md +++ b/wiki/deactivation_handler.md @@ -1,6 +1,6 @@ # Deactivation handler -It is possible to add an activation handler for a type not binded in transient scope. The deactivation handler is invoked before the type is unbinded from the container: +It is possible to add a deactivation handler for a type binded in singleton scope. The handler can be synchronous or asynchronous. The deactivation handler is invoked before the type is unbinded from the container: ```ts @injectable() @@ -24,7 +24,9 @@ It's possible to add a deactivation handler in multiple ways - Adding the handler to a binding. - Adding the handler to the class through the [preDestroy decorator](./pre_destroy.md). -Handlers added to the container are the firsts ones to be resolved. Any handler added to a child container is called before the ones added to their parent. Any handler added through the `preDestroy` decorator is called after any handler added to a container or a biding: +Handlers added to the container are the firsts ones to be resolved. Any handler added to a child container is called before the ones added to their parent. Relevant bindings from the container are called next and finally the `preDestroy` method is called. In the example above, relevant bindings are those bindings bound to the unbinded "Destroyable" service identifer. + +The example below demonstrates call order. ```ts let roll = 1; diff --git a/wiki/environment.md b/wiki/environment.md index 966bec641..1a3b4a923 100644 --- a/wiki/environment.md +++ b/wiki/environment.md @@ -43,8 +43,12 @@ This will create the Reflect object as a global. Most modern JavaScript engines support map but if you need to support old browsers you will need to use a map polyfill (e.g. [es6-map](https://www.npmjs.com/package/es6-map)). ## Promise -[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) are only required only if you use want to -[inject a provider](https://github.com/inversify/InversifyJS#injecting-a-provider-asynchronous-factory). +[Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) are required if you want to: + +[Inject a provider](https://github.com/inversify/InversifyJS/blob/master/wiki/provider_injection.md) or +[inject dynamic values asynchronously](https://github.com/inversify/InversifyJS/blob/master/wiki/value_injection.md). + +Handle [post construction](https://github.com/inversify/InversifyJS/blob/master/wiki/post_construct.md) and [activation](https://github.com/inversify/InversifyJS/blob/master/wiki/activation_handler.md), or [pre destroy](https://github.com/inversify/InversifyJS/blob/master/wiki/pre_destroy.md) and [deactivation](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md) asynchronously. Most modern JavaScript engines support promises but if you need to support old browsers you will need to use a promise polyfill (e.g. [es6-promise](https://github.com/stefanpenner/es6-promise) or [bluebird](https://www.npmjs.com/package/bluebird)). diff --git a/wiki/post_construct.md b/wiki/post_construct.md index 958fb29e2..9b695f3b1 100644 --- a/wiki/post_construct.md +++ b/wiki/post_construct.md @@ -9,6 +9,8 @@ Its some other cases it gives you a contract that guarantees that this method will be invoked only once in the lifetime of the object when used in singleton scope. See the following examples for usage. +The method can be synchronous or asynchronous. + ```ts interface Katana { diff --git a/wiki/value_injection.md b/wiki/value_injection.md index 7f6ced4be..3e8b8c12a 100644 --- a/wiki/value_injection.md +++ b/wiki/value_injection.md @@ -6,4 +6,6 @@ container.bind("Katana").toConstantValue(new Katana()); Binds an abstraction to a dynamic value: ```ts container.bind("Katana").toDynamicValue((context: interfaces.Context) => { return new Katana(); }); +// a dynamic value can return a promise that will resolve to the value +container.bind("Katana").toDynamicValue((context: interfaces.Context) => { return Promise.resolve(new Katana()); }); ``` From 70be077b25662fdbebc3c1b95de8b43fa2bb7889 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Sun, 25 Apr 2021 20:15:14 +0100 Subject: [PATCH 50/64] rebindAsync, missing module functionality and wiki updates --- src/bindings/binding.ts | 2 +- src/container/container.ts | 97 +++++++--- src/container/container_snapshot.ts | 5 +- src/container/lookup.ts | 15 +- src/container/module_activations_store.ts | 52 +++++ src/interfaces/interfaces.ts | 29 ++- test/container/container.test.ts | 49 +++++ test/container/container_module.test.ts | 181 +++++++++++++++++- test/container/lookup.test.ts | 4 +- .../module_activations_store.test.ts | 73 +++++++ wiki/container_api.md | 24 +-- wiki/container_modules.md | 14 +- 12 files changed, 486 insertions(+), 59 deletions(-) create mode 100644 src/container/module_activations_store.ts create mode 100644 test/container/module_activations_store.test.ts diff --git a/src/bindings/binding.ts b/src/bindings/binding.ts index b709c5207..34601fb0c 100644 --- a/src/bindings/binding.ts +++ b/src/bindings/binding.ts @@ -5,7 +5,7 @@ import { id } from "../utils/id"; class Binding implements interfaces.Binding { public id: number; - public moduleId: string; + public moduleId: interfaces.ContainerModuleBase["id"]; // Determines weather the bindings has been already activated // The activation action takes place when an instance is resolved diff --git a/src/container/container.ts b/src/container/container.ts index 39a0746c1..2f18bf6f7 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -12,6 +12,7 @@ import { id } from "../utils/id"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; import { Lookup } from "./lookup"; +import { ModuleActivationsStore } from "./module_activations_store"; type GetArgs = Omit @@ -27,6 +28,7 @@ class Container implements interfaces.Container { private _snapshots: interfaces.ContainerSnapshot[]; private _metadataReader: interfaces.MetadataReader; private _appliedMiddleware: interfaces.Middleware[] = []; + private _moduleActivationStore: interfaces.ModuleActivationsStore public static merge( container1: interfaces.Container, @@ -104,6 +106,7 @@ class Container implements interfaces.Container { this._deactivations = new Lookup>(); this.parent = null; this._metadataReader = new MetadataReader(); + this._moduleActivationStore = new ModuleActivationsStore() } public load(...modules: interfaces.ContainerModule[]) { @@ -119,6 +122,7 @@ class Container implements interfaces.Container { containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, containerModuleHelpers.rebindFunction, + containerModuleHelpers.unbindAsyncFunction, containerModuleHelpers.onActivationFunction, containerModuleHelpers.onDeactivationFunction ); @@ -140,6 +144,7 @@ class Container implements interfaces.Container { containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, containerModuleHelpers.rebindFunction, + containerModuleHelpers.unbindAsyncFunction, containerModuleHelpers.onActivationFunction, containerModuleHelpers.onDeactivationFunction ); @@ -149,17 +154,25 @@ class Container implements interfaces.Container { } public unload(...modules: interfaces.ContainerModule[]): void { - - const conditionFactory = (expected: any) => (item: interfaces.Binding): boolean => - item.moduleId === expected; - modules.forEach((module) => { - const condition = conditionFactory(module.id); - this._bindingDictionary.removeByCondition(condition); + const deactivations = this._removeModuleBindings(module.id) + this._deactivateSingletons(deactivations); + + this._removeModuleHandlers(module.id); }); } + public async unloadAsync(...modules: interfaces.ContainerModule[]): Promise { + for(const module of modules){ + const deactivations = this._removeModuleBindings(module.id) + await this._deactivateSingletonsAsync(deactivations) + + this._removeModuleHandlers(module.id); + } + + } + // Registers a type binding public bind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax { const scope = this.options.defaultScope || BindingScopeEnum.Transient; @@ -173,6 +186,11 @@ class Container implements interfaces.Container { return this.bind(serviceIdentifier); } + public async rebindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise> { + await this.unbindAsync(serviceIdentifier); + return this.bind(serviceIdentifier); + } + // Removes a type binding from the registry by its key public unbind(serviceIdentifier: interfaces.ServiceIdentifier): void { if (this._bindingDictionary.hasKey(serviceIdentifier)) { @@ -260,7 +278,8 @@ class Container implements interfaces.Container { this._bindingDictionary.clone(), this._middleware, this._activations.clone(), - this._deactivations.clone() + this._deactivations.clone(), + this._moduleActivationStore.clone() )); } @@ -273,6 +292,7 @@ class Container implements interfaces.Container { this._activations = snapshot.activations; this._deactivations = snapshot.deactivations; this._middleware = snapshot.middleware; + this._moduleActivationStore = snapshot.moduleActivationStore } public createChild(containerOptions?: interfaces.ContainerOptions): Container { @@ -386,6 +406,19 @@ class Container implements interfaces.Container { return instance[data.value](); } } + private _removeModuleHandlers(moduleId:number): void { + const handlers = this._moduleActivationStore.remove(moduleId) + if (handlers.onActivations.length > 0) { + this._activations.removeByCondition(onActivation => handlers.onActivations.indexOf(onActivation) !== -1); + } + if (handlers.onDeactivations.length > 0) { + this._deactivations.removeByCondition(onDeactivation => handlers.onDeactivations.indexOf(onDeactivation) !== -1); + } + } + + private _removeModuleBindings(moduleId:number): interfaces.Binding[] { + return this._bindingDictionary.removeByCondition(binding => binding.moduleId === moduleId); + } private _deactivate(binding: Binding, instance: T): void | Promise { const constructor = Object.getPrototypeOf(instance).constructor; @@ -459,45 +492,59 @@ class Container implements interfaces.Container { private _getContainerModuleHelpersFactory() { - const setModuleId = (bindingToSyntax: any, moduleId: number) => { + const setModuleId = (bindingToSyntax: any, moduleId: interfaces.ContainerModuleBase["id"]) => { bindingToSyntax._binding.moduleId = moduleId; }; - const getBindFunction = (moduleId: number) => + const getBindFunction = (moduleId: interfaces.ContainerModuleBase["id"]) => (serviceIdentifier: interfaces.ServiceIdentifier) => { - const _bind = this.bind.bind(this); - const bindingToSyntax = _bind(serviceIdentifier); + const bindingToSyntax = this.bind(serviceIdentifier); setModuleId(bindingToSyntax, moduleId); return bindingToSyntax; }; - const getUnbindFunction = (moduleId: number) => + const getUnbindFunction = () => (serviceIdentifier: interfaces.ServiceIdentifier) => { - const _unbind = this.unbind.bind(this); - _unbind(serviceIdentifier); + return this.unbind(serviceIdentifier); }; - const getIsboundFunction = (moduleId: number) => + const getUnbindAsyncFunction = () => + (serviceIdentifier: interfaces.ServiceIdentifier) => { + return this.unbindAsync(serviceIdentifier); + }; + + const getIsboundFunction = () => (serviceIdentifier: interfaces.ServiceIdentifier) => { - const _isBound = this.isBound.bind(this); - return _isBound(serviceIdentifier); + return this.isBound(serviceIdentifier) }; - const getRebindFunction = (moduleId: number) => + const getRebindFunction = (moduleId: interfaces.ContainerModuleBase["id"]) => (serviceIdentifier: interfaces.ServiceIdentifier) => { - const _rebind = this.rebind.bind(this); - const bindingToSyntax = _rebind(serviceIdentifier); + const bindingToSyntax = this.rebind(serviceIdentifier); setModuleId(bindingToSyntax, moduleId); return bindingToSyntax; }; - return (mId: number) => ({ + const getOnActivationFunction = (moduleId:interfaces.ContainerModuleBase["id"]) => + (serviceIdentifier: interfaces.ServiceIdentifier, onActivation: interfaces.BindingActivation) => { + this._moduleActivationStore.addActivation(moduleId,onActivation) + this.onActivation(serviceIdentifier,onActivation) + } + + const getOnDeactivationFunction = (moduleId:interfaces.ContainerModuleBase["id"]) => + (serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) => { + this._moduleActivationStore.addDeactivation(moduleId,onDeactivation) + this.onDeactivation(serviceIdentifier,onDeactivation) + } + + return (mId: interfaces.ContainerModuleBase["id"]) => ({ bindFunction: getBindFunction(mId), - isboundFunction: getIsboundFunction(mId), - onActivationFunction: this.onActivation.bind(this), - onDeactivationFunction: this.onDeactivation.bind(this), + isboundFunction: getIsboundFunction(), + onActivationFunction: getOnActivationFunction(mId), + onDeactivationFunction: getOnDeactivationFunction(mId), rebindFunction: getRebindFunction(mId), - unbindFunction: getUnbindFunction(mId) + unbindFunction: getUnbindFunction(), + unbindAsyncFunction: getUnbindAsyncFunction() }); } diff --git a/src/container/container_snapshot.ts b/src/container/container_snapshot.ts index 53b3fcdb4..94ead6cc7 100644 --- a/src/container/container_snapshot.ts +++ b/src/container/container_snapshot.ts @@ -6,18 +6,21 @@ class ContainerSnapshot implements interfaces.ContainerSnapshot { public activations: interfaces.Lookup>; public deactivations: interfaces.Lookup>; public middleware: interfaces.Next | null; + public moduleActivationStore: interfaces.ModuleActivationsStore; public static of( bindings: interfaces.Lookup>, middleware: interfaces.Next | null, activations: interfaces.Lookup>, - deactivations: interfaces.Lookup> + deactivations: interfaces.Lookup>, + moduleActivationsStore: interfaces.ModuleActivationsStore ) { const snapshot = new ContainerSnapshot(); snapshot.bindings = bindings; snapshot.middleware = middleware; snapshot.deactivations = deactivations; snapshot.activations = activations; + snapshot.moduleActivationStore = moduleActivationsStore; return snapshot; } diff --git a/src/container/lookup.ts b/src/container/lookup.ts index 0ac442938..fe43bb2b4 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -29,7 +29,6 @@ class Lookup implements interfaces.Lookup { const entry = this._map.get(serviceIdentifier); if (entry !== undefined) { entry.push(value); - this._map.set(serviceIdentifier, entry); } else { this._map.set(serviceIdentifier, [value]); } @@ -64,15 +63,25 @@ class Lookup implements interfaces.Lookup { } - public removeByCondition(condition: (item: T) => boolean): void { + public removeByCondition(condition: (item: T) => boolean): T[] { + const removals:T[] = []; this._map.forEach((entries, key) => { - const updatedEntries = entries.filter((entry) => !condition(entry)); + const updatedEntries:T[] = []; + for(const entry of entries){ + const remove = condition(entry) + if(remove){ + removals.push(entry) + }else{ + updatedEntries.push(entry) + } + } if (updatedEntries.length > 0) { this._map.set(key, updatedEntries); } else { this._map.delete(key); } }); + return removals; } // returns true if _map contains a key (serviceIdentifier) diff --git a/src/container/module_activations_store.ts b/src/container/module_activations_store.ts new file mode 100644 index 000000000..27b6c44a3 --- /dev/null +++ b/src/container/module_activations_store.ts @@ -0,0 +1,52 @@ +import { interfaces } from "../interfaces/interfaces"; + +export class ModuleActivationsStore implements interfaces.ModuleActivationsStore { + private _map = new Map(); + remove(moduleId: interfaces.ContainerModuleBase["id"]): interfaces.ModuleActivationHandlers { + if (this._map.has(moduleId)) { + const handlers = this._map.get(moduleId); + this._map.delete(moduleId); + return handlers!; + } + return this._getHandlersStore(); + } + + addDeactivation(moduleId: interfaces.ContainerModuleBase["id"], onDeactivation: interfaces.BindingDeactivation) { + const entry = this._map.get(moduleId); + if (entry !== undefined) { + entry.onDeactivations.push(onDeactivation); + } else { + const handlersStore = this._getHandlersStore(); + handlersStore.onDeactivations.push(onDeactivation); + this._map.set(moduleId, handlersStore); + } + } + + addActivation(moduleId: interfaces.ContainerModuleBase["id"], onActivation: interfaces.BindingActivation) { + const entry = this._map.get(moduleId); + if (entry !== undefined) { + entry.onActivations.push(onActivation); + } else { + const handlersStore = this._getHandlersStore(); + handlersStore.onActivations.push(onActivation); + this._map.set(moduleId, handlersStore); + } + } + + clone(): interfaces.ModuleActivationsStore { + const clone = new ModuleActivationsStore(); + this._map.forEach((handlersStore, moduleId) => { + handlersStore.onActivations.forEach(onActivation => clone.addActivation(moduleId,onActivation)); + handlersStore.onDeactivations.forEach(onDeactivation => clone.addDeactivation(moduleId,onDeactivation)); + }) + return clone; + } + + private _getHandlersStore(): interfaces.ModuleActivationHandlers { + const handlersStore: interfaces.ModuleActivationHandlers = { + onActivations: [], + onDeactivations: [] + }; + return handlersStore; + } +} diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 98572f789..431560487 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -54,7 +54,7 @@ namespace interfaces { export interface Binding extends Clonable> { id: number; - moduleId: string; + moduleId: ContainerModuleBase["id"]; activated: boolean; serviceIdentifier: ServiceIdentifier; constraint: ConstraintFunction; @@ -176,6 +176,7 @@ namespace interfaces { options: ContainerOptions; bind(serviceIdentifier: ServiceIdentifier): BindingToSyntax; rebind(serviceIdentifier: interfaces.ServiceIdentifier): interfaces.BindingToSyntax; + rebindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise> unbind(serviceIdentifier: ServiceIdentifier): void; unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise; unbindAll(): void; @@ -201,6 +202,7 @@ namespace interfaces { load(...modules: ContainerModule[]): void; loadAsync(...modules: AsyncContainerModule[]): Promise; unload(...modules: ContainerModule[]): void; + unloadAsync(...modules: interfaces.ContainerModule[]): Promise applyCustomMetadataReader(metadataReader: MetadataReader): void; applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; @@ -214,23 +216,39 @@ namespace interfaces { export type Unbind = (serviceIdentifier: ServiceIdentifier) => void; + export type UnbindAsync = (serviceIdentifier: ServiceIdentifier) => Promise; + export type IsBound = (serviceIdentifier: ServiceIdentifier) => boolean; - export interface ContainerModule { + export interface ContainerModuleBase{ id: number; + } + + export interface ContainerModule extends ContainerModuleBase { registry: ContainerModuleCallBack; } - export interface AsyncContainerModule { - id: number; + export interface AsyncContainerModule extends ContainerModuleBase { registry: AsyncContainerModuleCallBack; } + export interface ModuleActivationHandlers{ + onActivations:interfaces.BindingActivation[], + onDeactivations:interfaces.BindingDeactivation[] + } + + export interface ModuleActivationsStore extends Clonable{ + addDeactivation(moduleId: ContainerModuleBase["id"], onDeactivation: interfaces.BindingDeactivation): void + addActivation(moduleId: ContainerModuleBase["id"], onActivation: interfaces.BindingActivation): void + remove(moduleId: ContainerModuleBase["id"]): ModuleActivationHandlers + } + export type ContainerModuleCallBack = ( bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind, + unbindAsync: interfaces.UnbindAsync, onActivation: interfaces.Container["onActivation"], onDeactivation: interfaces.Container["onDeactivation"] ) => void; @@ -242,6 +260,7 @@ namespace interfaces { activations: Lookup>; deactivations: Lookup>; middleware: Next | null; + moduleActivationStore: interfaces.ModuleActivationsStore; } export interface Lookup extends Clonable> { @@ -249,7 +268,7 @@ namespace interfaces { getMap(): Map, T[]>; get(serviceIdentifier: ServiceIdentifier): T[]; remove(serviceIdentifier: interfaces.ServiceIdentifier): void; - removeByCondition(condition: (item: T) => boolean): void; + removeByCondition(condition: (item: T) => boolean): T[]; hasKey(serviceIdentifier: ServiceIdentifier): boolean; clone(): Lookup; traverse(func: (key: interfaces.ServiceIdentifier, value: T[]) => void): void; diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 5368e7d61..2868b2b7a 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -334,6 +334,33 @@ describe("Container", () => { expect(timesCalled).to.be.equal(1); }); + it("Should save and restore the container activations and deactivations when snapshot and restore", () => { + const sid = "sid"; + const container = new Container(); + container.bind(sid).toConstantValue("Value"); + + let activated = false; + let deactivated = false + + container.snapshot(); + + container.onActivation(sid,(c, i) => { + activated = true; + return i; + }); + container.onDeactivation(sid,i => { + deactivated = true; + }); + + container.restore(); + + container.get(sid); + container.unbind(sid); + + expect(activated).to.equal(false); + expect(deactivated).to.equal(false); + }) + it("Should be able to check is there are bindings available for a given identifier", () => { interface Warrior {} @@ -896,6 +923,28 @@ describe("Container", () => { }); + it("Should be able to override a binding using rebindAsync", async () => { + + const TYPES = { + someType: "someType" + }; + + const container = new Container(); + container.bind(TYPES.someType).toConstantValue(1); + container.bind(TYPES.someType).toConstantValue(2); + container.onDeactivation(TYPES.someType,() => Promise.resolve()) + + const values1 = container.getAll(TYPES.someType); + expect(values1[0]).to.eq(1); + expect(values1[1]).to.eq(2); + + (await container.rebindAsync(TYPES.someType)).toConstantValue(3); + const values2 = container.getAll(TYPES.someType); + expect(values2[0]).to.eq(3); + expect(values2[1]).to.eq(undefined); + + }); + it("Should be able to resolve named multi-injection (async)", async () => { interface Intl { diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index 888e1c4ee..fa89e2afc 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -1,5 +1,6 @@ import { expect } from "chai"; import * as sinon from "sinon"; +import { NOT_REGISTERED } from "../../src/constants/error_msgs"; import { Container } from "../../src/container/container"; import { AsyncContainerModule, ContainerModule } from "../../src/container/container_module"; import { interfaces } from "../../src/interfaces/interfaces"; @@ -125,23 +126,23 @@ describe("ContainerModule", () => { container.bind("A").toDynamicValue(() => "1"); expect(container.get("A")).to.eql("1"); - const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation) => { + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation) => { + bind("B").toConstantValue("2").onActivation(()=>"C"); onActivation("A", () => "B"); }); - container.load(warriors); + container.load(module); expect(container.get("A")).to.eql("B"); - + expect(container.get("B")).to.eql("C") }); - it("Should be able to add an deactivation hook through a container module", () => { + it("Should be able to add a deactivation hook through a container module", () => { const container = new Container(); container.bind("A").toConstantValue("1"); let deact = false; - - const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + const warriors = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { onDeactivation("A", () => { deact = true; }); @@ -160,7 +161,7 @@ describe("ContainerModule", () => { let deact = false; - const warriors = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + const warriors = new ContainerModule((bind, unbind, isBound, rebind, unBindAsync, onActivation, onDeactivation) => { onDeactivation("A", async () => { deact = true; }); @@ -180,7 +181,7 @@ describe("ContainerModule", () => { const serviceIdentifier = "destroyable"; const container = new Container(); - const containerModule = new ContainerModule((bind, unbind, isBound, rebind, onActivation, onDeactivation) => { + const containerModule = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { onDeactivation(serviceIdentifier, onActivationHandlerSpy); onDeactivation(serviceIdentifier, onActivationHandlerSpy); }); @@ -195,4 +196,168 @@ describe("ContainerModule", () => { expect(onActivationHandlerSpy.callCount).to.eq(2); }); + + it("Should remove module bindings when unload", () => { + const sid = "sid"; + const container = new Container(); + container.bind(sid).toConstantValue("Not module"); + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + bind(sid).toConstantValue("Module") + }); + container.load(module); + let values = container.getAll(sid); + expect(values).to.deep.equal(["Not module","Module"]); + + container.unload(module); + values = container.getAll(sid); + expect(values).to.deep.equal(["Not module"]); + }); + + it("Should deactivate singletons from module bindings when unload", () => { + const sid = "sid"; + const container = new Container(); + let moduleBindingDeactivated: string | undefined + let containerDeactivated: string | undefined + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + bind(sid).toConstantValue("Module").onDeactivation(injectable => {moduleBindingDeactivated = injectable}); + onDeactivation(sid,injectable => {containerDeactivated = injectable}) + }); + container.load(module); + container.get(sid); + + container.unload(module); + expect(moduleBindingDeactivated).to.equal("Module"); + expect(containerDeactivated).to.equal("Module"); + }); + + it("Should remove container handlers from module when unload", () => { + const sid = "sid"; + const container = new Container(); + let activatedNotModule: string | undefined + let deactivatedNotModule: string | undefined + container.onActivation(sid,(_,injected) => { + activatedNotModule = injected; + return injected; + }); + container.onDeactivation(sid, injected => {deactivatedNotModule = injected}) + container.bind(sid).toConstantValue("Value"); + let activationCount = 0; + let deactivationCount = 0; + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + onDeactivation(sid,_ => {deactivationCount++}); + onActivation(sid, (_,injected) => { + activationCount++; + return injected; + }); + }); + container.load(module); + container.unload(module); + + container.get(sid); + container.unbind(sid); + + expect(activationCount).to.equal(0); + expect(deactivationCount).to.equal(0); + + expect(activatedNotModule).to.equal("Value"); + expect(deactivatedNotModule).to.equal("Value") + }) + + it("Should remove module bindings when unloadAsync",async () => { + const sid = "sid"; + const container = new Container(); + container.onDeactivation(sid,injected => Promise.resolve()); + container.bind(sid).toConstantValue("Not module"); + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + bind(sid).toConstantValue("Module") + }); + container.load(module); + let values = container.getAll(sid); + expect(values).to.deep.equal(["Not module","Module"]); + + await container.unloadAsync(module); + values = container.getAll(sid); + expect(values).to.deep.equal(["Not module"]); + }); + + it("Should deactivate singletons from module bindings when unloadAsync", async () => { + const sid = "sid"; + const container = new Container(); + let moduleBindingDeactivated: string | undefined + let containerDeactivated: string | undefined + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + bind(sid).toConstantValue("Module").onDeactivation(injectable => {moduleBindingDeactivated = injectable}); + onDeactivation(sid,injectable => { + containerDeactivated = injectable; + return Promise.resolve(); + }) + }); + container.load(module); + container.get(sid); + + await container.unloadAsync(module); + expect(moduleBindingDeactivated).to.equal("Module"); + expect(containerDeactivated).to.equal("Module"); + }); + + it("Should remove container handlers from module when unloadAsync", async () => { + const sid = "sid"; + const container = new Container(); + let activatedNotModule: string | undefined + let deactivatedNotModule: string | undefined + container.onActivation(sid,(_,injected) => { + activatedNotModule = injected; + return injected; + }); + container.onDeactivation(sid, injected => { + deactivatedNotModule = injected; + }) + container.bind(sid).toConstantValue("Value"); + let activationCount = 0; + let deactivationCount = 0; + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + onDeactivation(sid,_ => { + deactivationCount++ + return Promise.resolve(); + }); + onActivation(sid, (_,injected) => { + activationCount++; + return injected; + }); + }); + container.load(module); + await container.unloadAsync(module); + + container.get(sid); + container.unbind(sid); + + expect(activationCount).to.equal(0); + expect(deactivationCount).to.equal(0); + + expect(activatedNotModule).to.equal("Value"); + expect(deactivatedNotModule).to.equal("Value") + }) + + it("should be able to unbindAsync from a module", async () => { + let _unbindAsync:interfaces.UnbindAsync | undefined + const container = new Container(); + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => { + _unbindAsync = unbindAsync + }); + const sid = "sid"; + container.bind(sid).toConstantValue("Value") + container.bind(sid).toConstantValue("Value2") + const deactivated:string[] = [] + container.onDeactivation(sid,injected => { + deactivated.push(injected); + return Promise.resolve(); + }) + + container.getAll(sid); + container.load(module); + await _unbindAsync!(sid); + expect(deactivated).to.deep.equal(["Value","Value2"]); + //bindings removed + expect(() => container.getAll(sid)).to.throw(`${NOT_REGISTERED} sid`) + }) }); diff --git a/test/container/lookup.test.ts b/test/container/lookup.test.ts index 4fc6cb1f2..92e2a60c2 100644 --- a/test/container/lookup.test.ts +++ b/test/container/lookup.test.ts @@ -106,8 +106,8 @@ describe("Lookup", () => { it("Should be able to remove a binding by a condition", () => { - const moduleId1 = "moduleId1"; - const moduleId2 = "moduleId2"; + const moduleId1 = 1; + const moduleId2 = 2; const warriorId = "Warrior"; const weaponId = "Weapon"; diff --git a/test/container/module_activations_store.test.ts b/test/container/module_activations_store.test.ts new file mode 100644 index 000000000..88f93ed46 --- /dev/null +++ b/test/container/module_activations_store.test.ts @@ -0,0 +1,73 @@ +import { expect } from "chai"; +import {ModuleActivationsStore} from "../../src/container/module_activations_store" +import { interfaces } from "../../src/inversify" + +describe("ModuleActivationsStore", () => { + it("should remove handlers added by the module", () => { + const moduleActivationsStore = new ModuleActivationsStore(); + const onActivation1: interfaces.BindingActivation = (c,a) => a; + const onActivation2: interfaces.BindingActivation = (c,a) => a; + const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); + const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); + moduleActivationsStore.addActivation(1,onActivation1); + moduleActivationsStore.addActivation(1,onActivation2); + moduleActivationsStore.addDeactivation(1,onDeactivation1); + moduleActivationsStore.addDeactivation(1,onDeactivation2); + + const onActivationMod2: interfaces.BindingActivation = (c,a) => a; + const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); + moduleActivationsStore.addActivation(2,onActivationMod2) + moduleActivationsStore.addDeactivation(2,onDeactivationMod2) + + const handlers = moduleActivationsStore.remove(1); + expect(handlers.onActivations).to.deep.equal([onActivation1, onActivation2]) + expect(handlers.onDeactivations).to.deep.equal([onDeactivation1, onDeactivation2]) + + const noHandlers = moduleActivationsStore.remove(1); + expect(noHandlers.onActivations.length).to.be.equal(0); + expect(noHandlers.onDeactivations.length).to.be.equal(0); + + const module2Handlers = moduleActivationsStore.remove(2); + expect(module2Handlers.onActivations).to.deep.equal([onActivationMod2]); + expect(module2Handlers.onDeactivations).to.deep.equal([onDeactivationMod2]); + }) + + it("should be able to clone", () => { + const moduleActivationsStore = new ModuleActivationsStore(); + const onActivation1: interfaces.BindingActivation = (c,a) => a; + const onActivation2: interfaces.BindingActivation = (c,a) => a; + const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); + const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); + moduleActivationsStore.addActivation(1,onActivation1); + moduleActivationsStore.addActivation(1,onActivation2); + moduleActivationsStore.addDeactivation(1,onDeactivation1); + moduleActivationsStore.addDeactivation(1,onDeactivation2); + + const onActivationMod2: interfaces.BindingActivation = (c,a) => a; + const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); + moduleActivationsStore.addActivation(2,onActivationMod2) + moduleActivationsStore.addDeactivation(2,onDeactivationMod2) + + const clone = moduleActivationsStore.clone() + + //change original + const onActivation3: interfaces.BindingActivation = (c,a) => a; + const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); + moduleActivationsStore.addActivation(1,onActivation3) + moduleActivationsStore.addDeactivation(1,onDeactivation3); + moduleActivationsStore.remove(2); + + const cloneModule1Handlers = clone.remove(1); + const expectedModule1Handlers:interfaces.ModuleActivationHandlers = { + onActivations:[onActivation1, onActivation2], + onDeactivations:[onDeactivation1,onDeactivation2] + } + expect(cloneModule1Handlers).to.deep.equal(expectedModule1Handlers) + const cloneModule2Handlers = clone.remove(2) + const expectedModule2Handlers:interfaces.ModuleActivationHandlers = { + onActivations:[onActivationMod2], + onDeactivations:[onDeactivationMod2] + } + expect(cloneModule2Handlers).to.deep.equal(expectedModule2Handlers) + }) +}) \ No newline at end of file diff --git a/wiki/container_api.md b/wiki/container_api.md index 76f0e3195..8de081307 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -99,7 +99,7 @@ expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Samurai).name).to.eql("S expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Katana).name).to.eql("Katana"); ``` -## container.get(serviceIdentifier: ServiceIdentifier) +## container.get(serviceIdentifier: ServiceIdentifier): T Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -110,7 +110,7 @@ container.bind("Weapon").to(Katana); let katana = container.get("Weapon"); ``` -## container.getAsync(serviceIdentifier: ServiceIdentifier) +## container.getAsync(serviceIdentifier: ServiceIdentifier): Promise Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -125,7 +125,7 @@ container.bind("Level1").toDynamicValue(() => buildLevel1()); let level1 = await container.getAsync("Level1"); // Returns Promise ``` -## container.getNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol) +## container.getNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): T Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -138,7 +138,7 @@ let katana = container.getNamed("Weapon", "japanese"); let shuriken = container.getNamed("Weapon", "chinese"); ``` -## container.getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol) +## container.getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -151,7 +151,7 @@ let katana = container.getNamedAsync("Weapon", "japanese"); let shuriken = container.getNamedAsync("Weapon", "chinese"); ``` -## container.getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) +## container.getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: @@ -164,7 +164,7 @@ let katana = container.getTagged("Weapon", "faction", "samurai"); let shuriken = container.getTagged("Weapon", "faction", "ninja"); ``` -## container.getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) +## container.getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -177,7 +177,7 @@ let katana = container.getTaggedAsync("Weapon", "faction", "samurai"); let shuriken = container.getTaggedAsync("Weapon", "faction", "ninja"); ``` -## container.getAll(serviceIdentifier: interfaces.ServiceIdentifier) +## container.getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] Get all available bindings for a given identifier. All the bindings must be syncronously resolved, otherwise an error is thrown: @@ -189,7 +189,7 @@ container.bind("Weapon").to(Shuriken); let weapons = container.getAll("Weapon"); // returns Weapon[] ``` -## container.getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier) +## container.getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise Get all available bindings for a given identifier: @@ -201,7 +201,7 @@ container.bind("Weapon").toDynamicValue(async () => new Shuriken()); let weapons = await container.getAllAsync("Weapon"); // returns Promise ``` -## container.getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol) +## container.getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be syncronously resolved, otherwise an error is thrown: @@ -230,7 +230,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol) +## container.getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise Resolves all the dependencies by its runtime identifier that matches the given named constraint: @@ -260,7 +260,7 @@ expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) +## container.getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] Resolves all the dependencies by its runtime identifier that matches the given tagged constraint. All the binding must be syncronously resolved, otherwise an error is thrown: @@ -289,7 +289,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any) +## container.getAllTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise Resolves all the dependencies by its runtime identifier that matches the given tagged constraint: diff --git a/wiki/container_modules.md b/wiki/container_modules.md index 217ba2aff..14fb620d1 100644 --- a/wiki/container_modules.md +++ b/wiki/container_modules.md @@ -1,6 +1,10 @@ # Declaring container modules Container modules can help you to manage the complexity of your bindings in very large applications. +The container module constructor argument is a registration callback that is passed functions that behave the same as +the methods of the Container class. +When a container module is unloaded from a Container the bindings added by that container will be removed and the [deactivation process](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md) will occur for each of them. Container deactivation and [activation handlers](https://github.com/inversify/InversifyJS/blob/master/wiki/activation_handler.md) will also be removed. +Use the unloadAsync method to unload when there will be an async deactivation handler or async [pre destroy](https://github.com/inversify/InversifyJS/blob/master/wiki/pre_destroy.md) ## Synchronous container modules @@ -14,7 +18,10 @@ let weapons = new ContainerModule( bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, - rebind: interfaces.Rebind + rebind: interfaces.Rebind, + unbindAsync: interfaces.UnbindAsync, + onActivation: interfaces.Container["onActivation"], + onDeactivation: interfaces.Container["onDeactivation"] ) => { bind("Katana").to(Katana); bind("Shuriken").to(Shuriken); @@ -39,7 +46,10 @@ let weapons = new AsyncContainerModule( bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, - rebind: interfaces.Rebind + rebind: interfaces.Rebind, + unbindAsync: interfaces.UnbindAsync, + onActivation: interfaces.Container["onActivation"], + onDeactivation: interfaces.Container["onDeactivation"] ) => { bind("Katana").to(Katana); bind("Shuriken").to(Shuriken); From f707594a0e5c53a0ac7fa976b3e5495b9190b76e Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Mon, 26 Apr 2021 11:12:09 +0100 Subject: [PATCH 51/64] review changes - type name change and missing snapshot moduleActivationStore test --- src/container/container.ts | 6 +- src/container/container_snapshot.ts | 6 +- ...ns_store.ts => module_activation_store.ts} | 6 +- src/interfaces/interfaces.ts | 4 +- test/container/container.test.ts | 18 +++++ ...est.ts => module_activation_store.test.ts} | 66 +++++++++---------- 6 files changed, 62 insertions(+), 44 deletions(-) rename src/container/{module_activations_store.ts => module_activation_store.ts} (91%) rename test/container/{module_activations_store.test.ts => module_activation_store.test.ts} (60%) diff --git a/src/container/container.ts b/src/container/container.ts index 2f18bf6f7..0bea9cb07 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -12,7 +12,7 @@ import { id } from "../utils/id"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; import { Lookup } from "./lookup"; -import { ModuleActivationsStore } from "./module_activations_store"; +import { ModuleActivationStore } from "./module_activation_store"; type GetArgs = Omit @@ -28,7 +28,7 @@ class Container implements interfaces.Container { private _snapshots: interfaces.ContainerSnapshot[]; private _metadataReader: interfaces.MetadataReader; private _appliedMiddleware: interfaces.Middleware[] = []; - private _moduleActivationStore: interfaces.ModuleActivationsStore + private _moduleActivationStore: interfaces.ModuleActivationStore public static merge( container1: interfaces.Container, @@ -106,7 +106,7 @@ class Container implements interfaces.Container { this._deactivations = new Lookup>(); this.parent = null; this._metadataReader = new MetadataReader(); - this._moduleActivationStore = new ModuleActivationsStore() + this._moduleActivationStore = new ModuleActivationStore() } public load(...modules: interfaces.ContainerModule[]) { diff --git a/src/container/container_snapshot.ts b/src/container/container_snapshot.ts index 94ead6cc7..3ec52997d 100644 --- a/src/container/container_snapshot.ts +++ b/src/container/container_snapshot.ts @@ -6,21 +6,21 @@ class ContainerSnapshot implements interfaces.ContainerSnapshot { public activations: interfaces.Lookup>; public deactivations: interfaces.Lookup>; public middleware: interfaces.Next | null; - public moduleActivationStore: interfaces.ModuleActivationsStore; + public moduleActivationStore: interfaces.ModuleActivationStore; public static of( bindings: interfaces.Lookup>, middleware: interfaces.Next | null, activations: interfaces.Lookup>, deactivations: interfaces.Lookup>, - moduleActivationsStore: interfaces.ModuleActivationsStore + moduleActivationStore: interfaces.ModuleActivationStore ) { const snapshot = new ContainerSnapshot(); snapshot.bindings = bindings; snapshot.middleware = middleware; snapshot.deactivations = deactivations; snapshot.activations = activations; - snapshot.moduleActivationStore = moduleActivationsStore; + snapshot.moduleActivationStore = moduleActivationStore; return snapshot; } diff --git a/src/container/module_activations_store.ts b/src/container/module_activation_store.ts similarity index 91% rename from src/container/module_activations_store.ts rename to src/container/module_activation_store.ts index 27b6c44a3..306ea3818 100644 --- a/src/container/module_activations_store.ts +++ b/src/container/module_activation_store.ts @@ -1,6 +1,6 @@ import { interfaces } from "../interfaces/interfaces"; -export class ModuleActivationsStore implements interfaces.ModuleActivationsStore { +export class ModuleActivationStore implements interfaces.ModuleActivationStore { private _map = new Map(); remove(moduleId: interfaces.ContainerModuleBase["id"]): interfaces.ModuleActivationHandlers { if (this._map.has(moduleId)) { @@ -33,8 +33,8 @@ export class ModuleActivationsStore implements interfaces.ModuleActivationsStore } } - clone(): interfaces.ModuleActivationsStore { - const clone = new ModuleActivationsStore(); + clone(): interfaces.ModuleActivationStore { + const clone = new ModuleActivationStore(); this._map.forEach((handlersStore, moduleId) => { handlersStore.onActivations.forEach(onActivation => clone.addActivation(moduleId,onActivation)); handlersStore.onDeactivations.forEach(onDeactivation => clone.addDeactivation(moduleId,onDeactivation)); diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 431560487..355ae561e 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -237,7 +237,7 @@ namespace interfaces { onDeactivations:interfaces.BindingDeactivation[] } - export interface ModuleActivationsStore extends Clonable{ + export interface ModuleActivationStore extends Clonable{ addDeactivation(moduleId: ContainerModuleBase["id"], onDeactivation: interfaces.BindingDeactivation): void addActivation(moduleId: ContainerModuleBase["id"], onActivation: interfaces.BindingActivation): void remove(moduleId: ContainerModuleBase["id"]): ModuleActivationHandlers @@ -260,7 +260,7 @@ namespace interfaces { activations: Lookup>; deactivations: Lookup>; middleware: Next | null; - moduleActivationStore: interfaces.ModuleActivationsStore; + moduleActivationStore: interfaces.ModuleActivationStore; } export interface Lookup extends Clonable> { diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 2868b2b7a..bfebcf46a 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -6,6 +6,7 @@ import * as ERROR_MSGS from "../../src/constants/error_msgs"; import { BindingScopeEnum } from "../../src/constants/literal_types"; import { Container } from "../../src/container/container"; import { ContainerModule } from "../../src/container/container_module"; +import { ModuleActivationStore } from "../../src/container/module_activation_store"; import { interfaces } from "../../src/interfaces/interfaces"; import { getBindingDictionary } from "../../src/planning/planner"; import { getServiceIdentifierAsString } from "../../src/utils/serialization"; @@ -361,6 +362,23 @@ describe("Container", () => { expect(deactivated).to.equal(false); }) + it("Should save and restore the module activation store when snapshot and restore", () => { + const container = new Container(); + const clonedActivationStore = new ModuleActivationStore(); + const originalActivationStore = { + clone(){ + return clonedActivationStore; + } + } + const anyContainer = container as any; + anyContainer._moduleActivationStore = originalActivationStore; + container.snapshot(); + const snapshot:interfaces.ContainerSnapshot = anyContainer._snapshots[0]; + expect(snapshot.moduleActivationStore === clonedActivationStore).to.equal(true); + container.restore(); + expect(anyContainer._moduleActivationStore === clonedActivationStore).to.equal(true); + }) + it("Should be able to check is there are bindings available for a given identifier", () => { interface Warrior {} diff --git a/test/container/module_activations_store.test.ts b/test/container/module_activation_store.test.ts similarity index 60% rename from test/container/module_activations_store.test.ts rename to test/container/module_activation_store.test.ts index 88f93ed46..86b514a4a 100644 --- a/test/container/module_activations_store.test.ts +++ b/test/container/module_activation_store.test.ts @@ -1,73 +1,73 @@ import { expect } from "chai"; -import {ModuleActivationsStore} from "../../src/container/module_activations_store" +import {ModuleActivationStore} from "../../src/container/module_activation_store" import { interfaces } from "../../src/inversify" -describe("ModuleActivationsStore", () => { +describe("ModuleActivationStore", () => { it("should remove handlers added by the module", () => { - const moduleActivationsStore = new ModuleActivationsStore(); + const moduleActivationStore = new ModuleActivationStore(); const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationsStore.addActivation(1,onActivation1); - moduleActivationsStore.addActivation(1,onActivation2); - moduleActivationsStore.addDeactivation(1,onDeactivation1); - moduleActivationsStore.addDeactivation(1,onDeactivation2); + moduleActivationStore.addActivation(1,onActivation1); + moduleActivationStore.addActivation(1,onActivation2); + moduleActivationStore.addDeactivation(1,onDeactivation1); + moduleActivationStore.addDeactivation(1,onDeactivation2); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationsStore.addActivation(2,onActivationMod2) - moduleActivationsStore.addDeactivation(2,onDeactivationMod2) + moduleActivationStore.addActivation(2,onActivationMod2); + moduleActivationStore.addDeactivation(2,onDeactivationMod2); - const handlers = moduleActivationsStore.remove(1); - expect(handlers.onActivations).to.deep.equal([onActivation1, onActivation2]) - expect(handlers.onDeactivations).to.deep.equal([onDeactivation1, onDeactivation2]) + const handlers = moduleActivationStore.remove(1); + expect(handlers.onActivations).to.deep.equal([onActivation1, onActivation2]); + expect(handlers.onDeactivations).to.deep.equal([onDeactivation1, onDeactivation2]); - const noHandlers = moduleActivationsStore.remove(1); + const noHandlers = moduleActivationStore.remove(1); expect(noHandlers.onActivations.length).to.be.equal(0); expect(noHandlers.onDeactivations.length).to.be.equal(0); - const module2Handlers = moduleActivationsStore.remove(2); + const module2Handlers = moduleActivationStore.remove(2); expect(module2Handlers.onActivations).to.deep.equal([onActivationMod2]); expect(module2Handlers.onDeactivations).to.deep.equal([onDeactivationMod2]); - }) + }); it("should be able to clone", () => { - const moduleActivationsStore = new ModuleActivationsStore(); + const moduleActivationStore = new ModuleActivationStore(); const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationsStore.addActivation(1,onActivation1); - moduleActivationsStore.addActivation(1,onActivation2); - moduleActivationsStore.addDeactivation(1,onDeactivation1); - moduleActivationsStore.addDeactivation(1,onDeactivation2); + moduleActivationStore.addActivation(1,onActivation1); + moduleActivationStore.addActivation(1,onActivation2); + moduleActivationStore.addDeactivation(1,onDeactivation1); + moduleActivationStore.addDeactivation(1,onDeactivation2); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationsStore.addActivation(2,onActivationMod2) - moduleActivationsStore.addDeactivation(2,onDeactivationMod2) + moduleActivationStore.addActivation(2,onActivationMod2); + moduleActivationStore.addDeactivation(2,onDeactivationMod2); - const clone = moduleActivationsStore.clone() + const clone = moduleActivationStore.clone(); //change original const onActivation3: interfaces.BindingActivation = (c,a) => a; const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationsStore.addActivation(1,onActivation3) - moduleActivationsStore.addDeactivation(1,onDeactivation3); - moduleActivationsStore.remove(2); + moduleActivationStore.addActivation(1,onActivation3); + moduleActivationStore.addDeactivation(1,onDeactivation3); + moduleActivationStore.remove(2); const cloneModule1Handlers = clone.remove(1); const expectedModule1Handlers:interfaces.ModuleActivationHandlers = { onActivations:[onActivation1, onActivation2], onDeactivations:[onDeactivation1,onDeactivation2] - } - expect(cloneModule1Handlers).to.deep.equal(expectedModule1Handlers) - const cloneModule2Handlers = clone.remove(2) + }; + expect(cloneModule1Handlers).to.deep.equal(expectedModule1Handlers); + const cloneModule2Handlers = clone.remove(2); const expectedModule2Handlers:interfaces.ModuleActivationHandlers = { onActivations:[onActivationMod2], onDeactivations:[onDeactivationMod2] - } - expect(cloneModule2Handlers).to.deep.equal(expectedModule2Handlers) - }) -}) \ No newline at end of file + }; + expect(cloneModule2Handlers).to.deep.equal(expectedModule2Handlers); + }); +}); From 00a16782b2c8c21014047ea8b3e969ff40cb57f2 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Mon, 26 Apr 2021 18:05:20 +0100 Subject: [PATCH 52/64] last code climate refactor --- src/container/container.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index 0bea9cb07..f7f3dde54 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -362,7 +362,7 @@ class Container implements interfaces.Container { public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { const getArgs = this._getAllArgs(serviceIdentifier); - return Promise.all(this._get(getArgs) as (Promise|T)[]); + return this._getAll(getArgs); } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { @@ -378,7 +378,7 @@ class Container implements interfaces.Container { ): Promise { const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); - return Promise.all(this._get(getArgs) as (Promise|T)[]); + return this._getAll(getArgs); } public getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] { @@ -548,7 +548,9 @@ class Container implements interfaces.Container { }); } - + private _getAll(getArgs:GetArgs): Promise{ + return Promise.all(this._get(getArgs) as (Promise|T)[]); + } // Prepares arguments required for resolution and // delegates resolution to _middleware if available // otherwise it delegates resolution to _planAndResolve From b118ab0bbf54f75a40ed4f4e3af22e8b856ae037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 27 Apr 2021 10:52:49 +0200 Subject: [PATCH 53/64] docs: update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754e1c1b9..297033b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project from 5.0.0 forward will be documented in thi The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Async bindings #1132 +- Async binding resolution (getAllAsync, getAllNamedAsync, getAllTaggedAsync, getAsync, getNamedAsync, getTaggedAsync, rebindAsync, unbindAsync, unbindAllAsync, unloadAsync) #1132 +- Global onActivation / onDeactivation #1132 +- Parent/Child onActivation / onDeactivation #1132 +- Module onActivation / onDeactivation #1132 +- Added @preDestroy decorator #1132 + +### Changed +- @postConstruct can target an asyncronous function #1132 + ## [5.1.1] - 2021-04-25 -Fix pre-publish for build artifacts From 6d525a29516b20e359a0e0f5ff3e2f2ae59f8d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Tue, 27 Apr 2021 19:07:46 +0200 Subject: [PATCH 54/64] feat: add lookup.removeIntersection --- src/container/container.ts | 23 +++--- src/container/lookup.ts | 47 +++++++++--- src/container/module_activation_store.ts | 67 ++++++++++------- src/interfaces/interfaces.ts | 19 +++-- .../container/module_activation_store.test.ts | 74 +++++++++++-------- 5 files changed, 143 insertions(+), 87 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index f7f3dde54..ab442bc44 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -406,17 +406,14 @@ class Container implements interfaces.Container { return instance[data.value](); } } - private _removeModuleHandlers(moduleId:number): void { - const handlers = this._moduleActivationStore.remove(moduleId) - if (handlers.onActivations.length > 0) { - this._activations.removeByCondition(onActivation => handlers.onActivations.indexOf(onActivation) !== -1); - } - if (handlers.onDeactivations.length > 0) { - this._deactivations.removeByCondition(onDeactivation => handlers.onDeactivations.indexOf(onDeactivation) !== -1); - } + private _removeModuleHandlers(moduleId: number): void { + const moduleActivationsHandlers = this._moduleActivationStore.remove(moduleId); + + this._activations.removeIntersection(moduleActivationsHandlers.onActivations); + this._deactivations.removeIntersection(moduleActivationsHandlers.onDeactivations); } - private _removeModuleBindings(moduleId:number): interfaces.Binding[] { + private _removeModuleBindings(moduleId: number): interfaces.Binding[] { return this._bindingDictionary.removeByCondition(binding => binding.moduleId === moduleId); } @@ -527,14 +524,14 @@ class Container implements interfaces.Container { const getOnActivationFunction = (moduleId:interfaces.ContainerModuleBase["id"]) => (serviceIdentifier: interfaces.ServiceIdentifier, onActivation: interfaces.BindingActivation) => { - this._moduleActivationStore.addActivation(moduleId,onActivation) - this.onActivation(serviceIdentifier,onActivation) + this._moduleActivationStore.addActivation(moduleId, serviceIdentifier, onActivation); + this.onActivation(serviceIdentifier, onActivation); } const getOnDeactivationFunction = (moduleId:interfaces.ContainerModuleBase["id"]) => (serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) => { - this._moduleActivationStore.addDeactivation(moduleId,onDeactivation) - this.onDeactivation(serviceIdentifier,onDeactivation) + this._moduleActivationStore.addDeactivation(moduleId, serviceIdentifier, onDeactivation); + this.onDeactivation(serviceIdentifier,onDeactivation); } return (mId: interfaces.ContainerModuleBase["id"]) => ({ diff --git a/src/container/lookup.ts b/src/container/lookup.ts index fe43bb2b4..464361f93 100644 --- a/src/container/lookup.ts +++ b/src/container/lookup.ts @@ -60,27 +60,42 @@ class Lookup implements interfaces.Lookup { if (!this._map.delete(serviceIdentifier)) { throw new Error(ERROR_MSGS.KEY_NOT_FOUND); } + } + + public removeIntersection(lookup: interfaces.Lookup): void { + this.traverse( + (serviceIdentifier: interfaces.ServiceIdentifier, value: T[]) => { + const lookupActivations = lookup.hasKey(serviceIdentifier) ? lookup.get(serviceIdentifier) : undefined; + if (lookupActivations !== undefined) { + const filteredValues = value.filter( + (lookupValue) => + !lookupActivations.some((moduleActivation) => lookupValue === moduleActivation) + ); + + this._setValue(serviceIdentifier, filteredValues); + } + } + ); } public removeByCondition(condition: (item: T) => boolean): T[] { - const removals:T[] = []; + const removals: T[] = []; this._map.forEach((entries, key) => { const updatedEntries:T[] = []; - for(const entry of entries){ - const remove = condition(entry) - if(remove){ - removals.push(entry) - }else{ - updatedEntries.push(entry) + + for (const entry of entries) { + const remove = condition(entry); + if (remove) { + removals.push(entry); + } else { + updatedEntries.push(entry); } } - if (updatedEntries.length > 0) { - this._map.set(key, updatedEntries); - } else { - this._map.delete(key); - } + + this._setValue(key, updatedEntries); }); + return removals; } @@ -113,6 +128,14 @@ class Lookup implements interfaces.Lookup { }); } + private _setValue(serviceIdentifier: interfaces.ServiceIdentifier, value: T[]): void { + if (value.length > 0) { + this._map.set(serviceIdentifier, value); + } else { + this._map.delete(serviceIdentifier); + } + } + } export { Lookup }; diff --git a/src/container/module_activation_store.ts b/src/container/module_activation_store.ts index 306ea3818..854b91e76 100644 --- a/src/container/module_activation_store.ts +++ b/src/container/module_activation_store.ts @@ -1,51 +1,64 @@ import { interfaces } from "../interfaces/interfaces"; +import { Lookup } from "./lookup"; export class ModuleActivationStore implements interfaces.ModuleActivationStore { private _map = new Map(); - remove(moduleId: interfaces.ContainerModuleBase["id"]): interfaces.ModuleActivationHandlers { + + public remove(moduleId: number): interfaces.ModuleActivationHandlers { if (this._map.has(moduleId)) { const handlers = this._map.get(moduleId); this._map.delete(moduleId); return handlers!; } - return this._getHandlersStore(); + return this._getEmptyHandlersStore(); } - addDeactivation(moduleId: interfaces.ContainerModuleBase["id"], onDeactivation: interfaces.BindingDeactivation) { - const entry = this._map.get(moduleId); - if (entry !== undefined) { - entry.onDeactivations.push(onDeactivation); - } else { - const handlersStore = this._getHandlersStore(); - handlersStore.onDeactivations.push(onDeactivation); - this._map.set(moduleId, handlersStore); - } + public addDeactivation( + moduleId: number, + serviceIdentifier: interfaces.ServiceIdentifier, + onDeactivation: interfaces.BindingDeactivation, + ) { + this._getModuleActivationHandlers(moduleId) + .onDeactivations.add(serviceIdentifier, onDeactivation); } - addActivation(moduleId: interfaces.ContainerModuleBase["id"], onActivation: interfaces.BindingActivation) { - const entry = this._map.get(moduleId); - if (entry !== undefined) { - entry.onActivations.push(onActivation); - } else { - const handlersStore = this._getHandlersStore(); - handlersStore.onActivations.push(onActivation); - this._map.set(moduleId, handlersStore); - } + public addActivation( + moduleId: number, + serviceIdentifier: interfaces.ServiceIdentifier, + onActivation: interfaces.BindingActivation, + ) { + this._getModuleActivationHandlers(moduleId) + .onActivations.add(serviceIdentifier, onActivation); } - clone(): interfaces.ModuleActivationStore { + public clone(): interfaces.ModuleActivationStore { const clone = new ModuleActivationStore(); + this._map.forEach((handlersStore, moduleId) => { - handlersStore.onActivations.forEach(onActivation => clone.addActivation(moduleId,onActivation)); - handlersStore.onDeactivations.forEach(onDeactivation => clone.addDeactivation(moduleId,onDeactivation)); - }) + clone._map.set(moduleId, { + onActivations: handlersStore.onActivations.clone(), + onDeactivations: handlersStore.onDeactivations.clone(), + }); + }); + return clone; } - private _getHandlersStore(): interfaces.ModuleActivationHandlers { + private _getModuleActivationHandlers(moduleId: number): interfaces.ModuleActivationHandlers { + let moduleActivationHandlers: interfaces.ModuleActivationHandlers | undefined = this._map.get(moduleId); + + if (moduleActivationHandlers === undefined) { + moduleActivationHandlers = this._getEmptyHandlersStore(); + this._map.set(moduleId, moduleActivationHandlers); + } + + return moduleActivationHandlers; + } + + private _getEmptyHandlersStore(): interfaces.ModuleActivationHandlers { const handlersStore: interfaces.ModuleActivationHandlers = { - onActivations: [], - onDeactivations: [] + onActivations: new Lookup(), + onDeactivations: new Lookup() }; return handlersStore; } diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 355ae561e..5c99b7e28 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -233,13 +233,21 @@ namespace interfaces { } export interface ModuleActivationHandlers{ - onActivations:interfaces.BindingActivation[], - onDeactivations:interfaces.BindingDeactivation[] + onActivations: Lookup>, + onDeactivations: Lookup> } - export interface ModuleActivationStore extends Clonable{ - addDeactivation(moduleId: ContainerModuleBase["id"], onDeactivation: interfaces.BindingDeactivation): void - addActivation(moduleId: ContainerModuleBase["id"], onActivation: interfaces.BindingActivation): void + export interface ModuleActivationStore extends Clonable { + addDeactivation( + moduleId: ContainerModuleBase["id"], + serviceIdentifier: ServiceIdentifier, + onDeactivation: interfaces.BindingDeactivation + ): void + addActivation( + moduleId: ContainerModuleBase["id"], + serviceIdentifier: ServiceIdentifier, + onActivation: interfaces.BindingActivation + ): void remove(moduleId: ContainerModuleBase["id"]): ModuleActivationHandlers } @@ -269,6 +277,7 @@ namespace interfaces { get(serviceIdentifier: ServiceIdentifier): T[]; remove(serviceIdentifier: interfaces.ServiceIdentifier): void; removeByCondition(condition: (item: T) => boolean): T[]; + removeIntersection(lookup: interfaces.Lookup): void hasKey(serviceIdentifier: ServiceIdentifier): boolean; clone(): Lookup; traverse(func: (key: interfaces.ServiceIdentifier, value: T[]) => void): void; diff --git a/test/container/module_activation_store.test.ts b/test/container/module_activation_store.test.ts index 86b514a4a..ffa17be69 100644 --- a/test/container/module_activation_store.test.ts +++ b/test/container/module_activation_store.test.ts @@ -5,69 +5,83 @@ import { interfaces } from "../../src/inversify" describe("ModuleActivationStore", () => { it("should remove handlers added by the module", () => { const moduleActivationStore = new ModuleActivationStore(); + const serviceIdentifier: string = 'some-service'; + const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1,onActivation1); - moduleActivationStore.addActivation(1,onActivation2); - moduleActivationStore.addDeactivation(1,onDeactivation1); - moduleActivationStore.addDeactivation(1,onDeactivation2); + moduleActivationStore.addActivation(1, serviceIdentifier, onActivation1); + moduleActivationStore.addActivation(1, serviceIdentifier, onActivation2); + moduleActivationStore.addDeactivation(1,serviceIdentifier, onDeactivation1); + moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation2); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(2,onActivationMod2); - moduleActivationStore.addDeactivation(2,onDeactivationMod2); + moduleActivationStore.addActivation(2, serviceIdentifier, onActivationMod2); + moduleActivationStore.addDeactivation(2, serviceIdentifier, onDeactivationMod2); const handlers = moduleActivationStore.remove(1); - expect(handlers.onActivations).to.deep.equal([onActivation1, onActivation2]); - expect(handlers.onDeactivations).to.deep.equal([onDeactivation1, onDeactivation2]); + expect(handlers.onActivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onActivation1, onActivation2]]])); + expect(handlers.onDeactivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onDeactivation1, onDeactivation2]]])); const noHandlers = moduleActivationStore.remove(1); - expect(noHandlers.onActivations.length).to.be.equal(0); - expect(noHandlers.onDeactivations.length).to.be.equal(0); + expect(noHandlers.onActivations.getMap()).to.deep.equal(new Map()); + expect(noHandlers.onDeactivations.getMap()).to.deep.equal(new Map()); const module2Handlers = moduleActivationStore.remove(2); - expect(module2Handlers.onActivations).to.deep.equal([onActivationMod2]); - expect(module2Handlers.onDeactivations).to.deep.equal([onDeactivationMod2]); + expect(module2Handlers.onActivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onActivationMod2]]])); + expect(module2Handlers.onDeactivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onDeactivationMod2]]])); }); it("should be able to clone", () => { const moduleActivationStore = new ModuleActivationStore(); + + const serviceIdentifier: string = 'some-service'; + const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1,onActivation1); - moduleActivationStore.addActivation(1,onActivation2); - moduleActivationStore.addDeactivation(1,onDeactivation1); - moduleActivationStore.addDeactivation(1,onDeactivation2); + + moduleActivationStore.addActivation(1, serviceIdentifier, onActivation1); + moduleActivationStore.addActivation(1, serviceIdentifier, onActivation2); + moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation1); + moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation2); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(2,onActivationMod2); - moduleActivationStore.addDeactivation(2,onDeactivationMod2); + moduleActivationStore.addActivation(2, serviceIdentifier, onActivationMod2); + moduleActivationStore.addDeactivation(2, serviceIdentifier, onDeactivationMod2); const clone = moduleActivationStore.clone(); //change original const onActivation3: interfaces.BindingActivation = (c,a) => a; const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1,onActivation3); - moduleActivationStore.addDeactivation(1,onDeactivation3); + + moduleActivationStore.addActivation(1, serviceIdentifier, onActivation3); + moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation3); moduleActivationStore.remove(2); const cloneModule1Handlers = clone.remove(1); - const expectedModule1Handlers:interfaces.ModuleActivationHandlers = { - onActivations:[onActivation1, onActivation2], - onDeactivations:[onDeactivation1,onDeactivation2] - }; - expect(cloneModule1Handlers).to.deep.equal(expectedModule1Handlers); + + expect(cloneModule1Handlers.onActivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier, [onActivation1, onActivation2]]]), + ); + + expect(cloneModule1Handlers.onDeactivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier, [onDeactivation1, onDeactivation2]]]), + ); + const cloneModule2Handlers = clone.remove(2); - const expectedModule2Handlers:interfaces.ModuleActivationHandlers = { - onActivations:[onActivationMod2], - onDeactivations:[onDeactivationMod2] - }; - expect(cloneModule2Handlers).to.deep.equal(expectedModule2Handlers); + + expect(cloneModule2Handlers.onActivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier, [onActivationMod2]]]), + ); + + expect(cloneModule2Handlers.onDeactivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier, [onDeactivationMod2]]]), + ); }); }); From e2f009965f32546ecad16c6c5ef88db9970a1921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 28 Apr 2021 12:27:16 +0200 Subject: [PATCH 55/64] test: add test for lookup.removeIntersection --- test/container/lookup.test.ts | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/container/lookup.test.ts b/test/container/lookup.test.ts index 92e2a60c2..0bae2e95f 100644 --- a/test/container/lookup.test.ts +++ b/test/container/lookup.test.ts @@ -177,4 +177,45 @@ describe("Lookup", () => { }); + it('should be able to remove the intersection with another lookup', () => { + const lookup = new Lookup(); + + const serviceIdentifier1 = 'service-identifier-1'; + const serviceIdentifier2 = 'service-identifier-2'; + + const serviceIdentifier1Values = [11, 12, 13, 14]; + const serviceIdentifier2Values = [21, 22, 23, 24]; + + for (const value of serviceIdentifier1Values) { + lookup.add(serviceIdentifier1, value); + } + + for (const value of serviceIdentifier2Values) { + lookup.add(serviceIdentifier2, value); + } + + const lookupToIntersect = new Lookup(); + + const lookupToIntersectServiceIdentifier2Values = [23, 24, 25, 26]; + + const serviceIdentifier3 = 'service-identifier-3'; + + const lookulToIntersectServiceIdentifier3Values = [31, 32, 33, 34]; + + for (const value of lookupToIntersectServiceIdentifier2Values) { + lookupToIntersect.add(serviceIdentifier2, value); + } + + for (const value of lookulToIntersectServiceIdentifier3Values) { + lookupToIntersect.add(serviceIdentifier3, value); + } + + lookup.removeIntersection(lookupToIntersect); + + expect(lookup.getMap()).to.deep.equal(new Map([ + [serviceIdentifier1, [...serviceIdentifier1Values]], + [serviceIdentifier2, [21, 22]], + ])); + }); + }); From 530eb865aafb738b5632e0b3325a0921f9b6ad78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 28 Apr 2021 15:57:18 +0200 Subject: [PATCH 56/64] test: improve module_activation_store tests --- test/container/lookup.test.ts | 4 +- .../container/module_activation_store.test.ts | 96 ++++++++++++------- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/test/container/lookup.test.ts b/test/container/lookup.test.ts index 0bae2e95f..539c0344d 100644 --- a/test/container/lookup.test.ts +++ b/test/container/lookup.test.ts @@ -200,13 +200,13 @@ describe("Lookup", () => { const serviceIdentifier3 = 'service-identifier-3'; - const lookulToIntersectServiceIdentifier3Values = [31, 32, 33, 34]; + const lookupToIntersectServiceIdentifier3Values = [31, 32, 33, 34]; for (const value of lookupToIntersectServiceIdentifier2Values) { lookupToIntersect.add(serviceIdentifier2, value); } - for (const value of lookulToIntersectServiceIdentifier3Values) { + for (const value of lookupToIntersectServiceIdentifier3Values) { lookupToIntersect.add(serviceIdentifier3, value); } diff --git a/test/container/module_activation_store.test.ts b/test/container/module_activation_store.test.ts index ffa17be69..9eb3d4a11 100644 --- a/test/container/module_activation_store.test.ts +++ b/test/container/module_activation_store.test.ts @@ -5,83 +5,111 @@ import { interfaces } from "../../src/inversify" describe("ModuleActivationStore", () => { it("should remove handlers added by the module", () => { const moduleActivationStore = new ModuleActivationStore(); - const serviceIdentifier: string = 'some-service'; + + const moduleId1: number = 1; + const moduleId2: number = 2; + const serviceIdentifier1: string = 'some-service-1'; + const serviceIdentifier2: string = 'some-service-2'; const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; + const onActivation3: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1, serviceIdentifier, onActivation1); - moduleActivationStore.addActivation(1, serviceIdentifier, onActivation2); - moduleActivationStore.addDeactivation(1,serviceIdentifier, onDeactivation1); - moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation2); + const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); + + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation1); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation2); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier2, onActivation3); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation1); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation2); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier2, onDeactivation3); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(2, serviceIdentifier, onActivationMod2); - moduleActivationStore.addDeactivation(2, serviceIdentifier, onDeactivationMod2); - - const handlers = moduleActivationStore.remove(1); - expect(handlers.onActivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onActivation1, onActivation2]]])); - expect(handlers.onDeactivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onDeactivation1, onDeactivation2]]])); - - const noHandlers = moduleActivationStore.remove(1); + moduleActivationStore.addActivation(moduleId2, serviceIdentifier1, onActivationMod2); + moduleActivationStore.addDeactivation(moduleId2, serviceIdentifier1, onDeactivationMod2); + + const handlers = moduleActivationStore.remove(moduleId1); + expect(handlers.onActivations.getMap()).to.deep.equal(new Map([ + [serviceIdentifier1, [onActivation1, onActivation2]], + [serviceIdentifier2, [onActivation3]] + ])); + expect(handlers.onDeactivations.getMap()).to.deep.equal(new Map([ + [serviceIdentifier1, [onDeactivation1, onDeactivation2]], + [serviceIdentifier2, [onDeactivation3]], + ])); + + const noHandlers = moduleActivationStore.remove(moduleId1); expect(noHandlers.onActivations.getMap()).to.deep.equal(new Map()); expect(noHandlers.onDeactivations.getMap()).to.deep.equal(new Map()); - const module2Handlers = moduleActivationStore.remove(2); - expect(module2Handlers.onActivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onActivationMod2]]])); - expect(module2Handlers.onDeactivations.getMap()).to.deep.equal(new Map([[serviceIdentifier, [onDeactivationMod2]]])); + const module2Handlers = moduleActivationStore.remove(moduleId2); + expect(module2Handlers.onActivations.getMap()).to.deep.equal(new Map([[serviceIdentifier1, [onActivationMod2]]])); + expect(module2Handlers.onDeactivations.getMap()).to.deep.equal(new Map([[serviceIdentifier1, [onDeactivationMod2]]])); }); it("should be able to clone", () => { const moduleActivationStore = new ModuleActivationStore(); - const serviceIdentifier: string = 'some-service'; + const moduleId1: number = 1; + const moduleId2: number = 2; + const serviceIdentifier1: string = 'some-service-1'; + const serviceIdentifier2: string = 'some-service-2'; const onActivation1: interfaces.BindingActivation = (c,a) => a; const onActivation2: interfaces.BindingActivation = (c,a) => a; + const onActivation3: interfaces.BindingActivation = (c,a) => a; const onDeactivation1: interfaces.BindingDeactivation = (d) => Promise.resolve(); const onDeactivation2: interfaces.BindingDeactivation = (d) => Promise.resolve(); + const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1, serviceIdentifier, onActivation1); - moduleActivationStore.addActivation(1, serviceIdentifier, onActivation2); - moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation1); - moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation2); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation1); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation2); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier2, onActivation3); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation1); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation2); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier2, onDeactivation3); const onActivationMod2: interfaces.BindingActivation = (c,a) => a; const onDeactivationMod2: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(2, serviceIdentifier, onActivationMod2); - moduleActivationStore.addDeactivation(2, serviceIdentifier, onDeactivationMod2); + moduleActivationStore.addActivation(moduleId2, serviceIdentifier1, onActivationMod2); + moduleActivationStore.addDeactivation(moduleId2, serviceIdentifier1, onDeactivationMod2); const clone = moduleActivationStore.clone(); //change original - const onActivation3: interfaces.BindingActivation = (c,a) => a; - const onDeactivation3: interfaces.BindingDeactivation = (d) => Promise.resolve(); + const onActivation4: interfaces.BindingActivation = (c,a) => a; + const onDeactivation4: interfaces.BindingDeactivation = (d) => Promise.resolve(); - moduleActivationStore.addActivation(1, serviceIdentifier, onActivation3); - moduleActivationStore.addDeactivation(1, serviceIdentifier, onDeactivation3); - moduleActivationStore.remove(2); + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation4); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation4); + moduleActivationStore.remove(moduleId2); - const cloneModule1Handlers = clone.remove(1); + const cloneModule1Handlers = clone.remove(moduleId1); expect(cloneModule1Handlers.onActivations.getMap()).to.deep.equal( - new Map([[serviceIdentifier, [onActivation1, onActivation2]]]), + new Map([ + [serviceIdentifier1, [onActivation1, onActivation2]], + [serviceIdentifier2, [onActivation3]] + ]), ); expect(cloneModule1Handlers.onDeactivations.getMap()).to.deep.equal( - new Map([[serviceIdentifier, [onDeactivation1, onDeactivation2]]]), + new Map([ + [serviceIdentifier1, [onDeactivation1, onDeactivation2]], + [serviceIdentifier2, [onDeactivation3]], + ]), ); - const cloneModule2Handlers = clone.remove(2); + const cloneModule2Handlers = clone.remove(moduleId2); expect(cloneModule2Handlers.onActivations.getMap()).to.deep.equal( - new Map([[serviceIdentifier, [onActivationMod2]]]), + new Map([[serviceIdentifier1, [onActivationMod2]]]), ); expect(cloneModule2Handlers.onDeactivations.getMap()).to.deep.equal( - new Map([[serviceIdentifier, [onDeactivationMod2]]]), + new Map([[serviceIdentifier1, [onDeactivationMod2]]]), ); }); }); From 748492e1fdda956f32368f1a85b434f43c3fe122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Wed, 28 Apr 2021 16:54:44 +0200 Subject: [PATCH 57/64] test: add test to ensure unbind async throws an error when no binding is found --- test/container/container.test.ts | 26 ++++++++++++++++++++++--- test/container/container_module.test.ts | 6 +++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/test/container/container.test.ts b/test/container/container.test.ts index bfebcf46a..5ba01d509 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -1,4 +1,4 @@ -import { expect } from "chai"; +import { assert, expect } from "chai"; import * as sinon from "sinon"; import { injectable } from "../../src/annotation/injectable"; import { postConstruct } from "../../src/annotation/post_construct"; @@ -128,9 +128,21 @@ describe("Container", () => { it("Should throw when cannot unbind", () => { const serviceIdentifier = "Ninja"; const container = new Container(); - const throwFunction = () => { container.unbind("Ninja"); }; + const throwFunction = () => { container.unbind(serviceIdentifier); }; expect(throwFunction).to.throw(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); - }); + }); + + it("Should throw when cannot unbind (async)", async () => { + const serviceIdentifier = "Ninja"; + const container = new Container(); + + try { + await container.unbindAsync(serviceIdentifier); + assert.fail(); + } catch (err: unknown) { + expect((err as Error).message).to.eql(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); + } + }); it("Should unbind a binding when requested", () => { @@ -1060,5 +1072,13 @@ describe("Container", () => { const services = await container.getAllAsync(serviceIdentifier); expect(services).to.deep.eq([firstValueBinded, secondValueBinded, thirdValueBinded]); + }); + + it('should throw an error if skipBaseClassChecks is not a boolean', () => { + expect(() => + new Container({ + skipBaseClassChecks: 'Jolene, Jolene, Jolene, Jolene' as unknown as boolean + }) + ).to.throw(ERROR_MSGS.CONTAINER_OPTIONS_INVALID_SKIP_BASE_CHECK); }) }); diff --git a/test/container/container_module.test.ts b/test/container/container_module.test.ts index fa89e2afc..3e784a125 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -335,8 +335,8 @@ describe("ContainerModule", () => { expect(deactivationCount).to.equal(0); expect(activatedNotModule).to.equal("Value"); - expect(deactivatedNotModule).to.equal("Value") - }) + expect(deactivatedNotModule).to.equal("Value"); + }); it("should be able to unbindAsync from a module", async () => { let _unbindAsync:interfaces.UnbindAsync | undefined @@ -359,5 +359,5 @@ describe("ContainerModule", () => { expect(deactivated).to.deep.equal(["Value","Value2"]); //bindings removed expect(() => container.getAll(sid)).to.throw(`${NOT_REGISTERED} sid`) - }) + }); }); From d2751a7dfa3d0ff9be410d7a7c7ee17f606f9fda Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 10:45:04 +0100 Subject: [PATCH 58/64] 100% coverage --- src/planning/planner.ts | 4 +--- test/container/container.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/planning/planner.ts b/src/planning/planner.ts index 9ebf6d749..d8fe492f8 100644 --- a/src/planning/planner.ts +++ b/src/planning/planner.ts @@ -246,9 +246,7 @@ function plan( if ( isStackOverflowExeption(error) ) { - if (context.plan) { - circularDependencyToException(context.plan.rootRequest); - } + circularDependencyToException(context.plan.rootRequest); } throw error; } diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 5ba01d509..45d93d168 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -580,6 +580,12 @@ describe("Container", () => { }); + it("Should default binding scope to Transient if no default scope on options", () => { + const container = new Container(); + container.options.defaultScope = undefined; + const expectedScope:interfaces.BindingScope = "Transient"; + expect((container.bind("SID") as any)._binding.scope).to.equal(expectedScope); + }); it("Should be able to configure automatic binding for @injectable() decorated classes", () => { @injectable() From 63a867903115060724e7dfb2ed15618e3e482bd7 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 10:47:42 +0100 Subject: [PATCH 59/64] correct test to match description --- test/resolution/resolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index a59b397ef..873747720 100644 --- a/test/resolution/resolver.test.ts +++ b/test/resolution/resolver.test.ts @@ -1349,10 +1349,10 @@ describe("Resolve", () => { it("Should only call parent async singleton once within child containers", async () => { const parent = new Container(); parent.bind("Parent").toDynamicValue(() => Promise.resolve(new Date())).inSingletonScope(); - + const child = parent.createChild(); const [subject1, subject2] = await Promise.all([ - parent.getAsync("Parent"), - parent.getAsync("Parent") + child.getAsync("Parent"), + child.getAsync("Parent") ]); expect(subject1 === subject2).eql(true); From 3ad854f9320ed4946c5351176f2f2bc245042272 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 11:17:17 +0100 Subject: [PATCH 60/64] wiki grammar correction --- wiki/container_api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiki/container_api.md b/wiki/container_api.md index 8de081307..7f92caa06 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -457,7 +457,7 @@ Please note that it only allows to skip declaring a binding for the root element ## container.onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation) -Adds an activation handler for the dependencies's identifier. +Adds an activation handler for all dependencies registered with the specified identifier. ```ts let container = new Container(); From dcf96ee0ea9c709a51854b56f91610094bd3a703 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 11:22:30 +0100 Subject: [PATCH 61/64] make container unload methods explicit that applies equally to async --- src/container/container.ts | 4 ++-- src/interfaces/interfaces.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index ab442bc44..f3392942e 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -153,7 +153,7 @@ class Container implements interfaces.Container { } - public unload(...modules: interfaces.ContainerModule[]): void { + public unload(...modules: interfaces.ContainerModuleBase[]): void { modules.forEach((module) => { const deactivations = this._removeModuleBindings(module.id) this._deactivateSingletons(deactivations); @@ -163,7 +163,7 @@ class Container implements interfaces.Container { } - public async unloadAsync(...modules: interfaces.ContainerModule[]): Promise { + public async unloadAsync(...modules: interfaces.ContainerModuleBase[]): Promise { for(const module of modules){ const deactivations = this._removeModuleBindings(module.id) await this._deactivateSingletonsAsync(deactivations) diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 5c99b7e28..81a97dfd3 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -201,8 +201,8 @@ namespace interfaces { resolve(constructorFunction: interfaces.Newable): T; load(...modules: ContainerModule[]): void; loadAsync(...modules: AsyncContainerModule[]): Promise; - unload(...modules: ContainerModule[]): void; - unloadAsync(...modules: interfaces.ContainerModule[]): Promise + unload(...modules: ContainerModuleBase[]): void; + unloadAsync(...modules: interfaces.ContainerModuleBase[]): Promise applyCustomMetadataReader(metadataReader: MetadataReader): void; applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; From 36cfea67b35ea1f0c355167143e6adcc3e6185c8 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 15:36:43 +0100 Subject: [PATCH 62/64] type changes --- src/container/container.ts | 4 ++-- src/interfaces/interfaces.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/container/container.ts b/src/container/container.ts index f3392942e..0b2346984 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -33,10 +33,10 @@ class Container implements interfaces.Container { public static merge( container1: interfaces.Container, container2: interfaces.Container, - ...container3: interfaces.Container[] + ...containers: interfaces.Container[] ): interfaces.Container { const container = new Container(); - const targetContainers: interfaces.Lookup>[] = [container1, container2, ...container3] + const targetContainers: interfaces.Lookup>[] = [container1, container2, ...containers] .map((targetContainer) => getBindingDictionary(targetContainer)); const bindingDictionary: interfaces.Lookup> = getBindingDictionary(container); diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 81a97dfd3..a882d56b8 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -202,12 +202,12 @@ namespace interfaces { load(...modules: ContainerModule[]): void; loadAsync(...modules: AsyncContainerModule[]): Promise; unload(...modules: ContainerModuleBase[]): void; - unloadAsync(...modules: interfaces.ContainerModuleBase[]): Promise + unloadAsync(...modules: ContainerModuleBase[]): Promise applyCustomMetadataReader(metadataReader: MetadataReader): void; applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; restore(): void; - createChild(): Container; + createChild(containerOptions?: interfaces.ContainerOptions): Container; } export type Bind = (serviceIdentifier: ServiceIdentifier) => BindingToSyntax; From f76a93346d01af7085835fc86b9897e81ffed343 Mon Sep 17 00:00:00 2001 From: Tony Hallett Date: Thu, 29 Apr 2021 15:36:56 +0100 Subject: [PATCH 63/64] wiki changes --- wiki/activation_handler.md | 2 +- wiki/architecture.md | 20 +++-- wiki/container_api.md | 169 ++++++++++++++++++++++++++--------- wiki/container_modules.md | 7 +- wiki/deactivation_handler.md | 2 +- wiki/pre_destroy.md | 2 +- 6 files changed, 148 insertions(+), 54 deletions(-) diff --git a/wiki/activation_handler.md b/wiki/activation_handler.md index 9e03918ce..e43ae9dd0 100644 --- a/wiki/activation_handler.md +++ b/wiki/activation_handler.md @@ -1,6 +1,6 @@ # Activation handler -It is possible to add an activation handler for a type. The activation handler is invoked after a dependency has been resolved and before it is added to a cache (if singleton or request singleton [see scope](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md)) and injected. The activation handler will not be invoked if type is resolved from a cache. The activation handler can be synchronous or asynchronous. +It is possible to add an activation handler for a type. The activation handler is invoked after a dependency has been resolved and before it is added to a cache (if singleton or request singleton - [see scope](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md)) and injected. The activation handler will not be invoked if the dependency is taken from a cache. The activation handler can be synchronous or asynchronous. Activation handlers are useful to keep our dependencies agnostic of the implementation of crosscutting concerns like caching or logging. diff --git a/wiki/architecture.md b/wiki/architecture.md index 5ba1ae2b6..7518b0908 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -13,7 +13,7 @@ InversifyJS performs **3 mandatory operations** before resolving a dependency: - **Resolution** - **Activation (optional)** -In some cases there will be some **additional operations (middleware & activation)**. +In some cases there will be some **additional operations (middleware & activation / deactivation)**. The project's folder architecture groups the components that are related with one of the phases of the resolution process: ``` @@ -52,16 +52,16 @@ The project's folder architecture groups the components that are related with on ``` ### Annotation Phase -The annotation phase reads the metadata generated by the decorators and transform it into a series of instances of the Request and Target classes. This Request and Target instances are then used to generate a resolution plan during the Planing Phase. +The annotation phase reads the metadata generated by the decorators and transform it into a series of instances of the Request and Target classes. This Request and Target instances are then used to generate a resolution plan during the Planning Phase. ### Planning Phase -When we invoke the following: +When we invoke a Container 'get' method, for instance: ```js var obj = container.get("SomeType"); ``` We start a new resolution, which means that the container will create a new resolution context. The resolution context contains a reference to the container and a reference to a Plan. -The Plan is generated by an instance of the Planner class. The Plan contains a reference to the context and a reference to the a (root) Request. A requests represents a dependency which will be injected into a Target. +The Plan contains a reference to the context and a reference to the a (root) Request. A request represents a dependency which will be injected into a Target. Let's take a look to the following code snippet: @@ -84,7 +84,7 @@ class FooBar implements FooBarInterface { var foobar = container.get("FooBarInterface"); ``` -The preceding code snippet will generate a new Context and a new Plan. The plan will contain a RootRequest with a null Target and two child Requests: +The preceding code snippet will generate a new Context and a new Plan. The plan will contain a root Request with a null Target and two child Requests: - The first child requests represents a dependency on `FooInterface` and its target is the constructor argument named `foo`. - The second child requests represents a dependency on `BarInterface` and its target is the constructor argument named `bar`. @@ -100,9 +100,15 @@ One example of middleware is the [inversify-logger-middleware](https://github.co ![](http://i.imgur.com/iFAogro.png) ### Resolution Phase -The Plan is passed to an instance of the Resolver class. The Resolver will then proceed to resolve each of the dependencies in the Request tree starting with the leafs and finishing with the root request. +The Plan is then used for resolution. The resolution will then proceed to resolve each of the dependencies in the Request tree starting with the leafs and finishing with the root request. The resolution process can be executed synchronously or asynchronously which can help to boost performance. ### Activation Phase -Activation takes place after a dependency has been resolved. Just before it is added to the cache (if singleton) and injected. It is possible to add an event handler that is invoked just before activation is completed. This feature allows developers to do things like injecting a proxy to intercept all the calls to the properties or methods of that object. +Activation takes place after a dependency has been resolved. Just before it is added to a cache (if singleton or request singleton - [see scope](https://github.com/inversify/InversifyJS/blob/master/wiki/scope.md))) and injected. It is possible to add an event handler that is invoked just before activation is completed. This feature allows developers to do things like injecting a proxy to intercept all the calls to the properties or methods of that object. +The [activation handler](https://github.com/inversify/InversifyJS/blob/master/wiki/activation_handler.md))) will not be invoked if type is resolved from a cache. The activation handler can be synchronous or asynchronous. + +### Deactivation Phase +The deactivation phase occurs when the Container methods unbind / unbindAsync / unbindAll / unbindAllAsync or invoked. Deactivation also occurs with [container modules](https://github.com/inversify/InversifyJS/blob/master/wiki/container_module.md))) that invoke the unbind and unbindAsync registration arguments or when a container module is unloaded from a Container. +It is possible to add a [deactivation handler](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md))) for a type binded in singleton scope. The handler can be synchronous or asynchronous. + diff --git a/wiki/container_api.md b/wiki/container_api.md index 7f92caa06..20d8f0063 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -1,25 +1,39 @@ # The Container API -The InversifyJS container provides some helpers to resolve multi-injections -and ambiguous bindings. +The InversifyJS container is where dependencies are first configured through bind and, possibly later, reconfigured and removed. The container can be worked on directly in this regard or container modules can be utilized. +You can query the configuration and resolve configured dependencies with resolved and the 'get' methods. +You can react to resolutions with container activation handlers and unbinding with container deactivation handlers. +You can create container hierarchies where container ascendants can supply the dependencies for descendants. +For testing, state can be saved as a snapshot on a stack and later restored. +For advanced control you can apply middleware to intercept the resolution request and the resolved dependency. +You can even provide your own annotation solution. + ## Container Options +Container options can be passed to the Container constructor and defaults will be provided if you do not or if you do but omit an option. +Options can be changed after construction and will be shared by child containers created from the Container if you do not provide options for them. ### defaultScope -The default scope is `transient` and you can change the scope of a type when declaring a binding: +The default scope is `transient` when binding to/toSelf/toDynamicValue/toService. +The other types of bindings are `singleton`. + +You can use container options to change the default scope for the bindings that default to `transient` at application level: ```ts -container.bind(TYPES.Warrior).to(Ninja).inSingletonScope(); -container.bind(TYPES.Warrior).to(Ninja).inTransientScope(); +let container = new Container({ defaultScope: "Singleton" }); ``` -You can use container options to change the default scope used at application level: +For all types of bindings you can change the scope when declaring: ```ts -let container = new Container({ defaultScope: "Singleton" }); +container.bind(TYPES.Warrior).to(Ninja).inSingletonScope(); +container.bind(TYPES.Warrior).to(Ninja).inTransientScope(); +container.bind(TYPES.Warrior).to(Ninja).inRequestScope(); ``` + + ### autoBindInjectable You can use this to activate automatic binding for `@injectable()` decorated classes: @@ -49,9 +63,9 @@ especially useful if any of your @injectable classes extend classes that you don let container = new Container({ skipBaseClassChecks: true }); ``` -## Container.merge(a: Container, b: Container) +## Container.merge(a: interfaces.Container, b: interfaces.Container, ...containers: interfaces.Container[]): interfaces.Container -Merges two containers into one: +Creates a new Container containing the bindings ( cloned bindings ) of two or more containers: ```ts @injectable() @@ -99,9 +113,23 @@ expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Samurai).name).to.eql("S expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Katana).name).to.eql("Katana"); ``` -## container.get(serviceIdentifier: ServiceIdentifier): T +## container.applyCustomMetadataReader(metadataReader: interfaces.MetadataReader): void + +An advanced feature.... See [middleware](https://github.com/inversify/InversifyJS/blob/master/wiki/middleware.md). +## container.applyMiddleware(...middleware: interfaces.Middleware[]): void -Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: +An advanced feature that can be used for cross cutting concerns. See [middleware](https://github.com/inversify/InversifyJS/blob/master/wiki/middleware.md). + +## container.bind\(serviceIdentifier: interfaces.ServiceIdentifier\): interfaces.BindingToSyntax\ + + + +## container.createChild(containerOptions?: interfaces.ContainerOptions): Container; + +Create a [container hierarchy ](https://github.com/inversify/InversifyJS/blob/master/wiki/hierarchical_di.md). If you do not provide options the child receives the options of the parent. +## container.get\(serviceIdentifier: interfaces.ServiceIdentifier\): T + +Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding and the binding must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -110,7 +138,7 @@ container.bind("Weapon").to(Katana); let katana = container.get("Weapon"); ``` -## container.getAsync(serviceIdentifier: ServiceIdentifier): Promise +## container.getAsync\(serviceIdentifier: interfaces.ServiceIdentifier\): Promise\ Resolves a dependency by its runtime identifier. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -125,9 +153,9 @@ container.bind("Level1").toDynamicValue(() => buildLevel1()); let level1 = await container.getAsync("Level1"); // Returns Promise ``` -## container.getNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): T +## container.getNamed\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): T -Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: +Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding and the binding must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -138,7 +166,7 @@ let katana = container.getNamed("Weapon", "japanese"); let shuriken = container.getNamed("Weapon", "chinese"); ``` -## container.getNamedAsync(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): Promise +## container.getNamedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): Promise\ Resolves a dependency by its runtime identifier that matches the given named constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -147,13 +175,13 @@ let container = new Container(); container.bind("Weapon").toDynamicValue(async () => new Katana()).whenTargetNamed("japanese"); container.bind("Weapon").toDynamicValue(async () => new Weapon()).whenTargetNamed("chinese"); -let katana = container.getNamedAsync("Weapon", "japanese"); -let shuriken = container.getNamedAsync("Weapon", "chinese"); +let katana = await container.getNamedAsync("Weapon", "japanese"); +let shuriken = await container.getNamedAsync("Weapon", "chinese"); ``` -## container.getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T +## container.getTagged\(serviceIdentifier: interfaces.ServiceIdentifier\, key: string | number | symbol, value: any): T -Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding and the binding must be syncronously resolved, otherwise an error is thrown: +Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding and the binding must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -164,7 +192,7 @@ let katana = container.getTagged("Weapon", "faction", "samurai"); let shuriken = container.getTagged("Weapon", "faction", "ninja"); ``` -## container.getTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise +## container.getTaggedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, key: string | number | symbol, value: any): Promise\ Resolves a dependency by its runtime identifier that matches the given tagged constraint. The runtime identifier must be associated with only one binding, otherwise an error is thrown: @@ -173,13 +201,13 @@ let container = new Container(); container.bind("Weapon").toDynamicValue(async () => new Katana()).whenTargetTagged("faction", "samurai"); container.bind("Weapon").toDynamicValue(async () => new Weapon()).whenTargetTagged("faction", "ninja"); -let katana = container.getTaggedAsync("Weapon", "faction", "samurai"); -let shuriken = container.getTaggedAsync("Weapon", "faction", "ninja"); +let katana = await container.getTaggedAsync("Weapon", "faction", "samurai"); +let shuriken = await container.getTaggedAsync("Weapon", "faction", "ninja"); ``` -## container.getAll(serviceIdentifier: interfaces.ServiceIdentifier): T[] +## container.getAll\(serviceIdentifier: interfaces.ServiceIdentifier\): T[] -Get all available bindings for a given identifier. All the bindings must be syncronously resolved, otherwise an error is thrown: +Get all available bindings for a given identifier. All the bindings must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -189,7 +217,7 @@ container.bind("Weapon").to(Shuriken); let weapons = container.getAll("Weapon"); // returns Weapon[] ``` -## container.getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise +## container.getAllAsync\(serviceIdentifier: interfaces.ServiceIdentifier\): Promise\ Get all available bindings for a given identifier: @@ -201,9 +229,9 @@ container.bind("Weapon").toDynamicValue(async () => new Shuriken()); let weapons = await container.getAllAsync("Weapon"); // returns Promise ``` -## container.getAllNamed(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): T[] +## container.getAllNamed\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): T[] -Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be syncronously resolved, otherwise an error is thrown: +Resolves all the dependencies by its runtime identifier that matches the given named constraint. All the binding must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -230,7 +258,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllNamedAsync(serviceIdentifier: interfaces.ServiceIdentifier, named: string | number | symbol): Promise +## container.getAllNamedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): Promise\ Resolves all the dependencies by its runtime identifier that matches the given named constraint: @@ -248,21 +276,21 @@ container.bind("Intl").toDynamicValue(async () => ({ goodbye: "au revoir" container.bind("Intl").toDynamicValue(async () => ({ hello: "hola" })).whenTargetNamed("es"); container.bind("Intl").toDynamicValue(async () => ({ goodbye: "adios" })).whenTargetNamed("es"); -let fr = container.getAllNamedAsync("Intl", "fr"); +let fr = await container.getAllNamedAsync("Intl", "fr"); expect(fr.length).to.eql(2); expect(fr[0].hello).to.eql("bonjour"); expect(fr[1].goodbye).to.eql("au revoir"); -let es = container.getAllNamedAsync("Intl", "es"); +let es = await container.getAllNamedAsync("Intl", "es"); expect(es.length).to.eql(2); expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] +## container.getAllTagged\(serviceIdentifier: interfaces.ServiceIdentifier\, key: string | number | symbol, value: any): T[] -Resolves all the dependencies by its runtime identifier that matches the given tagged constraint. All the binding must be syncronously resolved, otherwise an error is thrown: +Resolves all the dependencies by its runtime identifier that matches the given tagged constraint. All the binding must be synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -289,7 +317,7 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.getAllTaggedAsync(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): Promise +## container.getAllTaggedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, key: string | number | symbol, value: any): Promise\ Resolves all the dependencies by its runtime identifier that matches the given tagged constraint: @@ -307,18 +335,18 @@ container.bind("Intl").toDynamicValue(async () => ({ goodbye: "au revoir" container.bind("Intl").toDynamicValue(async () => ({ hello: "hola" })).whenTargetTagged("lang", "es"); container.bind("Intl").toDynamicValue(async () => ({ goodbye: "adios" })).whenTargetTagged("lang", "es"); -let fr = container.getAllTaggedAsync("Intl", "lang", "fr"); +let fr = await container.getAllTaggedAsync("Intl", "lang", "fr"); expect(fr.length).to.eql(2); expect(fr[0].hello).to.eql("bonjour"); expect(fr[1].goodbye).to.eql("au revoir"); -let es = container.getAllTaggedAsync("Intl", "lang", "es"); +let es = await container.getAllTaggedAsync("Intl", "lang", "es"); expect(es.length).to.eql(2); expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.isBound(serviceIdentifier: ServiceIdentifier) +## container.isBound(serviceIdentifier: interfaces.ServiceIdentifier\): boolean You can use the `isBound` method to check if there are registered bindings for a given service identifier. @@ -350,7 +378,7 @@ expect(container.isBound(katanaId)).to.eql(false); expect(container.isBound(katanaSymbol)).to.eql(false); ``` -## container.isBoundNamed(serviceIdentifier: ServiceIdentifier, named: string) +## container.isBoundNamed(serviceIdentifier: interfaces.ServiceIdentifier\, named: string): boolean You can use the `isBoundNamed` method to check if there are registered bindings for a given service identifier with a given named constraint. @@ -375,7 +403,7 @@ expect(container.isBoundNamed(zero, invalidDivisor)).to.eql(true); expect(container.isBoundNamed(zero, validDivisor)).to.eql(true); ``` -## container.isBoundTagged(serviceIdentifier: ServiceIdentifier, key: string, value: any) +## container.isBoundTagged(serviceIdentifier: interfaces.ServiceIdentifier\, key: string, value: any): boolean You can use the `isBoundTagged` method to check if there are registered bindings for a given service identifier with a given tagged constraint. @@ -399,7 +427,15 @@ expect(container.isBoundTagged(zero, isValidDivisor, false)).to.eql(true); expect(container.isBoundTagged(zero, isValidDivisor, true)).to.eql(true); ``` -## container.rebind(serviceIdentifier: ServiceIdentifier) +## container.load(...modules: interfaces.ContainerModule[]): void + +Calls the registration method of each module. See [container modules](https://github.com/inversify/InversifyJS/blob/master/wiki/container_modules.md) + +## container.loadAsync(...modules: interfaces.AsyncContainerModule[]): Promise\ + +As per load but for asynchronous registration. + +## container.rebind\(serviceIdentifier: interfaces.ServiceIdentifier\): : interfaces.BindingToSyntax\ You can use the `rebind` method to replace all the existing bindings for a given `serviceIdentifier`. The function returns an instance of `BindingToSyntax` which allows to create the replacement binding. @@ -423,7 +459,12 @@ expect(values2[0]).to.eq(3); expect(values2[1]).to.eq(undefined); ``` -## container.resolve(constructor: Newable) +## container.rebindAsync\(serviceIdentifier: interfaces.ServiceIdentifier\): Promise\> + +This is an asynchronous version of rebind. If you know deactivation is asynchronous then this should be used. +If you are not sure then use this method ! + +## container.resolve\(constructor: interfaces.Newable\): T Resolve is like `container.get(serviceIdentifier: ServiceIdentifier)` but it allows users to create an instance even if no bindings have been declared: ```ts @@ -455,7 +496,7 @@ expect(ninja.fight()).to.eql("cut!"); Please note that it only allows to skip declaring a binding for the root element in the dependency graph (composition root). All the sub-dependencies (e.g. `Katana` in the preceding example) will require a binding to be declared. -## container.onActivation(serviceIdentifier: ServiceIdentifier, onActivation: BindingActivation) +## container.onActivation\(serviceIdentifier: interfaces.ServiceIdentifier\, onActivation: interfaces.BindingActivation\): void Adds an activation handler for all dependencies registered with the specified identifier. @@ -470,7 +511,7 @@ container.onActivation("Weapon", (context: interfaces.Context, katana: Katana): let katana = container.get("Weapon"); ``` -## onDeactivation(serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) +## onDeactivation\(serviceIdentifier: interfaces.ServiceIdentifier\, onDeactivation: interfaces.BindingDeactivation\): void Adds a deactivation handler for the dependencie's identifier. @@ -483,3 +524,47 @@ container.onDeactivation("Weapon", (katana: Katana): void | Promise => { container.unbind("Weapon"); ``` +s + +## container.restore(): void; + +Restore container state to last snapshot. + +## container.snapshot(): void + +Save the state of the container to be later restored with the restore method. +## container.unbind(serviceIdentifier: interfaces.ServiceIdentifier\): void + +Remove all bindings binded in this container to the service identifer. This will result in the [deactivation process](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md). + +## container.unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier\): Promise\ + +This is the asynchronous version of unbind. If you know deactivation is asynchronous then this should be used. +If you are not sure then use this method ! + +## container.unbindAll(): void + +Remove all bindings binded in this container. This will result in the [deactivation process](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md). + +## container.unbindAllAsync(): Promise\ + +This is the asynchronous version of unbindAll. If you know deactivation is asynchronous then this should be used. +If you are not sure then use this method ! + +## container.unload(...modules: interfaces.ContainerModuleBase[]): void + +Removes bindings and handlers added by the modules. This will result in the [deactivation process](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md). +See [container modules](https://github.com/inversify/InversifyJS/blob/master/wiki/container_modules.md) + +## container.unloadAsync(...modules: interfaces.ContainerModuleBase[]): Promise\ + +Asynchronous version of unload. If you know deactivation is asynchronous then this should be used. +If you are not sure then use this method ! + +## container.parent: Container | null; + +Access the container hierarchy. + +## container.id: number + +An identifier auto generated to be unique. \ No newline at end of file diff --git a/wiki/container_modules.md b/wiki/container_modules.md index 14fb620d1..dc356d263 100644 --- a/wiki/container_modules.md +++ b/wiki/container_modules.md @@ -1,8 +1,11 @@ # Declaring container modules Container modules can help you to manage the complexity of your bindings in very large applications. -The container module constructor argument is a registration callback that is passed functions that behave the same as -the methods of the Container class. + +The constructor argument for ContainerModule and AsyncContainerModule is a registration callback that is passed functions that behave the same as the methods of the Container class. The registration callback for the AsyncContainerModule is asynchronous. + +When a container module is loaded into a Container the registration callback is invoked. This is the opportunity for the container module to register bindings and handlers. Use the Container load method for ContainerModule instances and the Container loadAsync method for AsyncContainerModule instances. + When a container module is unloaded from a Container the bindings added by that container will be removed and the [deactivation process](https://github.com/inversify/InversifyJS/blob/master/wiki/deactivation_handler.md) will occur for each of them. Container deactivation and [activation handlers](https://github.com/inversify/InversifyJS/blob/master/wiki/activation_handler.md) will also be removed. Use the unloadAsync method to unload when there will be an async deactivation handler or async [pre destroy](https://github.com/inversify/InversifyJS/blob/master/wiki/pre_destroy.md) diff --git a/wiki/deactivation_handler.md b/wiki/deactivation_handler.md index 62759205c..9dbdc339d 100644 --- a/wiki/deactivation_handler.md +++ b/wiki/deactivation_handler.md @@ -24,7 +24,7 @@ It's possible to add a deactivation handler in multiple ways - Adding the handler to a binding. - Adding the handler to the class through the [preDestroy decorator](./pre_destroy.md). -Handlers added to the container are the firsts ones to be resolved. Any handler added to a child container is called before the ones added to their parent. Relevant bindings from the container are called next and finally the `preDestroy` method is called. In the example above, relevant bindings are those bindings bound to the unbinded "Destroyable" service identifer. +Handlers added to the container are the first ones to be resolved. Any handler added to a child container is called before the ones added to their parent. Relevant bindings from the container are called next and finally the `preDestroy` method is called. In the example above, relevant bindings are those bindings bound to the unbinded "Destroyable" service identifer. The example below demonstrates call order. diff --git a/wiki/pre_destroy.md b/wiki/pre_destroy.md index a17f74edd..34a99c4b8 100644 --- a/wiki/pre_destroy.md +++ b/wiki/pre_destroy.md @@ -1,6 +1,6 @@ # Pre Destroy Decorator -It is possible to add a **@preDestroy** decorator for a class method. This decorator will run before a service is unbinded for any cached instance. For this reason, classes related to bindings on transient scope can not contain a method with this decorator sice there is no way to know which instances should be affected. +It is possible to add a **@preDestroy** decorator for a class method. This decorator will run before a service is unbinded for any cached instance. For this reason, only bindings in singleton scope can contain a method with this decorator. ```ts @injectable() From d06d0b05d8aeca4973b2505a9ecc1ca7a13402c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Pintos=20L=C3=B3pez?= Date: Thu, 29 Apr 2021 21:15:02 +0200 Subject: [PATCH 64/64] update interfaces.Container.createChild to avoid receiving parameters --- src/interfaces/interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index a882d56b8..9d2198291 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -207,7 +207,7 @@ namespace interfaces { applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; restore(): void; - createChild(containerOptions?: interfaces.ContainerOptions): Container; + createChild(): Container; } export type Bind = (serviceIdentifier: ServiceIdentifier) => BindingToSyntax;