diff --git a/.gitignore b/.gitignore index 9006f6de..3df7705a 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ test/**/*.js.map src/**/*.js.map src/*.js.map type_definitions/**/*.js +.DS_store .idea .nyc_output diff --git a/CHANGELOG.md b/CHANGELOG.md index 754e1c1b..297033b1 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 diff --git a/README.md b/README.md index a4be56eb..2e356b1d 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/src/annotation/post_construct.ts b/src/annotation/post_construct.ts index 355bb32a..4116cee8 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 new file mode 100644 index 00000000..97887ed8 --- /dev/null +++ b/src/annotation/pre_destroy.ts @@ -0,0 +1,10 @@ +import * as ERRORS_MSGS from "../constants/error_msgs"; +import * as METADATA_KEY from "../constants/metadata_keys"; +import { propertyEventDecorator } from "./property_event_decorator"; + +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 00000000..1cdec551 --- /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/bindings/binding.ts b/src/bindings/binding.ts index 9e2b15e4..34601fb0 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 @@ -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; @@ -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: interfaces.BindingActivation | null; + + // On deactivation handler (invoked just before an instance is unbinded and removed from container) + public onDeactivation: interfaces.BindingDeactivation | 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 08ed3bd6..3935b06b 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: 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 " + "used as service identifier"; @@ -39,11 +41,15 @@ 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 POST_CONSTRUCT_ERROR = (...values: any[]) => `@postConstruct 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` + - `service identifier '${values[1]}'.`; +export const ASYNC_UNBIND_REQUIRED = "Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"; +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}`; + +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"; diff --git a/src/constants/metadata_keys.ts b/src/constants/metadata_keys.ts index 0c795fac..e84b1034 100644 --- a/src/constants/metadata_keys.ts +++ b/src/constants/metadata_keys.ts @@ -31,6 +31,9 @@ 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"; + function getNonCustomTagKeys(): string[] { return [ INJECT_TAG, diff --git a/src/container/container.ts b/src/container/container.ts index 9b344f11..0b234698 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -7,10 +7,14 @@ 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, isPromiseOrContainsPromise } from "../utils/async"; import { id } from "../utils/id"; import { getServiceIdentifierAsString } from "../utils/serialization"; import { ContainerSnapshot } from "./container_snapshot"; import { Lookup } from "./lookup"; +import { ModuleActivationStore } from "./module_activation_store"; + +type GetArgs = Omit class Container implements interfaces.Container { @@ -19,17 +23,20 @@ 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; private _appliedMiddleware: interfaces.Middleware[] = []; + private _moduleActivationStore: interfaces.ModuleActivationStore 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); @@ -95,8 +102,11 @@ 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(); + this._moduleActivationStore = new ModuleActivationStore() } public load(...modules: interfaces.ContainerModule[]) { @@ -111,7 +121,10 @@ class Container implements interfaces.Container { containerModuleHelpers.bindFunction, containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, - containerModuleHelpers.rebindFunction + containerModuleHelpers.rebindFunction, + containerModuleHelpers.unbindAsyncFunction, + containerModuleHelpers.onActivationFunction, + containerModuleHelpers.onDeactivationFunction ); } @@ -130,25 +143,36 @@ class Container implements interfaces.Container { containerModuleHelpers.bindFunction, containerModuleHelpers.unbindFunction, containerModuleHelpers.isboundFunction, - containerModuleHelpers.rebindFunction + containerModuleHelpers.rebindFunction, + containerModuleHelpers.unbindAsyncFunction, + containerModuleHelpers.onActivationFunction, + containerModuleHelpers.onDeactivationFunction ); } } - public unload(...modules: interfaces.ContainerModule[]): void { - - const conditionFactory = (expected: any) => (item: interfaces.Binding): boolean => - item.moduleId === expected; - + public unload(...modules: interfaces.ContainerModuleBase[]): void { 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.ContainerModuleBase[]): 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; @@ -162,20 +186,61 @@ 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 { - try { - this._bindingDictionary.remove(serviceIdentifier); - } catch (e) { - throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); + if (this._bindingDictionary.hasKey(serviceIdentifier)) { + const bindings = this._bindingDictionary.get(serviceIdentifier); + + this._deactivateSingletons(bindings); + } + + this._removeServiceFromDictionary(serviceIdentifier); + } + + public async unbindAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + if (this._bindingDictionary.hasKey(serviceIdentifier)) { + const bindings = this._bindingDictionary.get(serviceIdentifier); + + await this._deactivateSingletonsAsync(bindings); } + + this._removeServiceFromDictionary(serviceIdentifier); } // Removes all the type bindings from the registry public unbindAll(): void { + this._bindingDictionary.traverse((key, value) => { + this._deactivateSingletons(value); + }); + this._bindingDictionary = new Lookup>(); } + public async unbindAllAsync(): Promise { + const promises: Promise[] = []; + + this._bindingDictionary.traverse((key, value) => { + promises.push(this._deactivateSingletonsAsync(value)); + }); + + 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); @@ -209,7 +274,13 @@ 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(), + this._moduleActivationStore.clone() + )); } public restore(): void { @@ -218,7 +289,10 @@ 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; + this._moduleActivationStore = snapshot.moduleActivationStore } public createChild(containerOptions?: interfaces.ContainerOptions): Container { @@ -243,31 +317,78 @@ 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; + const getArgs = this._getNotAllArgs(serviceIdentifier, false); + + return this._getButThrowIfAsync(getArgs) as T; + } + + public async getAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + const getArgs = this._getNotAllArgs(serviceIdentifier, false); + + return this._get(getArgs) as Promise|T; } public getTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T { - return this._get(false, false, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T; + const getArgs = this._getNotAllArgs(serviceIdentifier, false, key, value); + + return this._getButThrowIfAsync(getArgs) as T; + } + + 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|T; } 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[]; + const getArgs = this._getAllArgs(serviceIdentifier); + + return this._getButThrowIfAsync(getArgs) as T[]; + } + + public getAllAsync(serviceIdentifier: interfaces.ServiceIdentifier): Promise { + const getArgs = this._getAllArgs(serviceIdentifier); + + return this._getAll(getArgs); } public getAllTagged(serviceIdentifier: interfaces.ServiceIdentifier, key: string | number | symbol, value: any): T[] { - return this._get(false, true, TargetTypeEnum.Variable, serviceIdentifier, key, value) as T[]; + const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); + + return this._getButThrowIfAsync(getArgs) as T[]; + } + + public getAllTaggedAsync( + serviceIdentifier: interfaces.ServiceIdentifier, + key: string | number | symbol, + value: any + ): Promise { + const getArgs = this._getNotAllArgs(serviceIdentifier, true, key, value); + + return this._getAll(getArgs); } 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(); @@ -278,89 +399,218 @@ class Container implements interfaces.Container { return tempContainer.get(constructorFunction); } + 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); + + return instance[data.value](); + } + } + 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[] { + return this._bindingDictionary.removeByCondition(binding => binding.moduleId === moduleId); + } + + private _deactivate(binding: Binding, instance: T): void | Promise { + const constructor = Object.getPrototypeOf(instance).constructor; + + try { + if (this._deactivations.hasKey(binding.serviceIdentifier)) { + const result = this._deactivateContainer( + instance, + this._deactivations.get(binding.serviceIdentifier).values(), + ); + + if (isPromise(result)) { + return this._handleDeactivationError( + result.then(() => this._propagateContainerDeactivationThenBindingAndPreDestroyAsync( + binding, instance, constructor)), + constructor + ); + } + } + + const propagateDeactivationResult = this._propagateContainerDeactivationThenBindingAndPreDestroy( + binding, instance, constructor); + + if (isPromise(propagateDeactivationResult)) { + return this._handleDeactivationError(propagateDeactivationResult, constructor); + } + } catch (ex) { + throw new Error(ERROR_MSGS.ON_DEACTIVATION_ERROR(constructor.name, ex.message)); + } + } + + 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)); + } + } + + + private _deactivateContainer( + 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._deactivateContainerAsync(instance, deactivationsIterator), + ); + } + + deactivation = deactivationsIterator.next(); + } + } + + private async _deactivateContainerAsync( + instance: T, + deactivationsIterator: IterableIterator>, + ): Promise { + let deactivation = deactivationsIterator.next(); + + while (deactivation.value) { + await deactivation.value(instance); + deactivation = deactivationsIterator.next(); + } + } + 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, serviceIdentifier, onActivation); + this.onActivation(serviceIdentifier, onActivation); + } + + const getOnDeactivationFunction = (moduleId:interfaces.ContainerModuleBase["id"]) => + (serviceIdentifier: interfaces.ServiceIdentifier, onDeactivation: interfaces.BindingDeactivation) => { + this._moduleActivationStore.addDeactivation(moduleId, serviceIdentifier, onDeactivation); + this.onDeactivation(serviceIdentifier,onDeactivation); + } + + return (mId: interfaces.ContainerModuleBase["id"]) => ({ bindFunction: getBindFunction(mId), - isboundFunction: getIsboundFunction(mId), + isboundFunction: getIsboundFunction(), + onActivationFunction: getOnActivationFunction(mId), + onDeactivationFunction: getOnDeactivationFunction(mId), rebindFunction: getRebindFunction(mId), - unbindFunction: getUnbindFunction(mId) + unbindFunction: getUnbindFunction(), + unbindAsyncFunction: getUnbindAsyncFunction() }); } - + 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 - private _get( - avoidConstraints: boolean, - isMultiInject: boolean, - targetType: interfaces.TargetType, - serviceIdentifier: interfaces.ServiceIdentifier, - key?: string | number | symbol, - value?: any + private _get(getArgs: GetArgs): interfaces.ContainerResolution { + const planAndResolveArgs:interfaces.NextArgs = { + ...getArgs, + contextInterceptor:(context) => context, + targetType: TargetTypeEnum.Variable + } + if (this._middleware) { + const middlewareResult = this._middleware(planAndResolveArgs); + if (middlewareResult === undefined || middlewareResult === null) { + throw new Error(ERROR_MSGS.INVALID_MIDDLEWARE_RETURN); + } + return middlewareResult + } + + return this._planAndResolve()(planAndResolveArgs); + } + + private _getButThrowIfAsync( + getArgs: GetArgs, ): (T | T[]) { + const result = this._get(getArgs); - let result: (T | T[]) | null = null; + if (isPromiseOrContainsPromise(result)) { + throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); + } - const defaultArgs: interfaces.NextArgs = { - avoidConstraints, - contextInterceptor: (context: interfaces.Context) => context, - isMultiInject, - key, + return result as (T | T[]); + } + + private _getAllArgs(serviceIdentifier: interfaces.ServiceIdentifier): GetArgs { + const getAllArgs: GetArgs = { + avoidConstraints: true, + isMultiInject: true, serviceIdentifier, - targetType, - value }; - if (this._middleware) { - result = this._middleware(defaultArgs); - if (result === undefined || result === null) { - throw new Error(ERROR_MSGS.INVALID_MIDDLEWARE_RETURN); - } - } else { - result = this._planAndResolve()(defaultArgs); - } + return getAllArgs; + } + + private _getNotAllArgs( + serviceIdentifier: interfaces.ServiceIdentifier, + isMultiInject: boolean, + key?: string | number | symbol, + value?: any, + ): GetArgs { + const getNotAllArgs: GetArgs = { + avoidConstraints: false, + isMultiInject, + serviceIdentifier, + key, + value, + }; - return result; + 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) => interfaces.ContainerResolution { return (args: interfaces.NextArgs) => { // create a plan @@ -380,11 +630,98 @@ class Container implements interfaces.Container { // resolve plan const result = resolve(context); + return result; }; } + private _deactivateIfSingleton(binding: Binding): Promise | void { + if (!binding.cache) { + return; + } + + if (isPromise(binding.cache)) { + return binding.cache.then((resolved: any) => this._deactivate(binding, resolved)); + } + + return this._deactivate(binding, binding.cache); + } + + private _deactivateSingletons(bindings: Binding[]): void { + for (const binding of bindings) { + const result = this._deactivateIfSingleton(binding); + + if (isPromise(result)) { + throw new Error(ERROR_MSGS.ASYNC_UNBIND_REQUIRED); + } + } + } + + private async _deactivateSingletonsAsync(bindings: Binding[]): Promise { + await Promise.all(bindings.map(b => this._deactivateIfSingleton(b))) + } + + private _propagateContainerDeactivationThenBindingAndPreDestroy( + binding: Binding, + instance: T, + constructor: any + ): void | Promise { + if (this.parent) { + return this._deactivate.bind(this.parent)(binding, instance); + } else { + return this._bindingDeactivationAndPreDestroy(binding, instance, constructor); + } + } + + private async _propagateContainerDeactivationThenBindingAndPreDestroyAsync( + binding: Binding, + instance: T, + constructor: any + ): Promise { + if (this.parent) { + await this._deactivate.bind(this.parent)(binding, instance); + } else { + await this._bindingDeactivationAndPreDestroyAsync(binding, instance, constructor); + } + } + + private _removeServiceFromDictionary(serviceIdentifier: interfaces.ServiceIdentifier): void { + try { + this._bindingDictionary.remove(serviceIdentifier); + } catch (e) { + throw new Error(`${ERROR_MSGS.CANNOT_UNBIND} ${getServiceIdentifierAsString(serviceIdentifier)}`); + } + } + + private _bindingDeactivationAndPreDestroy( + 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._preDestroy(constructor, instance)); + } + } + + return this._preDestroy(constructor, instance); + } + + private async _bindingDeactivationAndPreDestroyAsync( + binding: Binding, + instance: T, + constructor: any + ): Promise { + if (typeof binding.onDeactivation === "function") { + await binding.onDeactivation(instance); + } + + await this._preDestroy(constructor, instance); + } + } export { Container }; diff --git a/src/container/container_snapshot.ts b/src/container/container_snapshot.ts index b86639de..3ec52997 100644 --- a/src/container/container_snapshot.ts +++ b/src/container/container_snapshot.ts @@ -3,12 +3,24 @@ 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 moduleActivationStore: interfaces.ModuleActivationStore; - 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>, + moduleActivationStore: interfaces.ModuleActivationStore + ) { const snapshot = new ContainerSnapshot(); snapshot.bindings = bindings; snapshot.middleware = middleware; + snapshot.deactivations = deactivations; + snapshot.activations = activations; + snapshot.moduleActivationStore = moduleActivationStore; return snapshot; } diff --git a/src/container/lookup.ts b/src/container/lookup.ts index 6194ce13..464361f9 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> implements interfaces.Lookup { +class Lookup implements interfaces.Lookup { // dictionary used store multiple values for each key private _map: Map, T[]>; @@ -28,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]); } @@ -60,18 +60,43 @@ 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): void { + public removeByCondition(condition: (item: T) => boolean): T[] { + const removals: T[] = []; this._map.forEach((entries, key) => { - const updatedEntries = entries.filter((entry) => !condition(entry)); - if (updatedEntries.length > 0) { - this._map.set(key, updatedEntries); - } else { - this._map.delete(key); + const updatedEntries:T[] = []; + + for (const entry of entries) { + const remove = condition(entry); + if (remove) { + removals.push(entry); + } else { + updatedEntries.push(entry); + } } + + this._setValue(key, updatedEntries); }); + + return removals; } // returns true if _map contains a key (serviceIdentifier) @@ -91,7 +116,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, isClonable(b) ? b.clone() : b)); }); return copy; @@ -103,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 new file mode 100644 index 00000000..854b91e7 --- /dev/null +++ b/src/container/module_activation_store.ts @@ -0,0 +1,65 @@ +import { interfaces } from "../interfaces/interfaces"; +import { Lookup } from "./lookup"; + +export class ModuleActivationStore implements interfaces.ModuleActivationStore { + private _map = new Map(); + + 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._getEmptyHandlersStore(); + } + + public addDeactivation( + moduleId: number, + serviceIdentifier: interfaces.ServiceIdentifier, + onDeactivation: interfaces.BindingDeactivation, + ) { + this._getModuleActivationHandlers(moduleId) + .onDeactivations.add(serviceIdentifier, onDeactivation); + } + + public addActivation( + moduleId: number, + serviceIdentifier: interfaces.ServiceIdentifier, + onActivation: interfaces.BindingActivation, + ) { + this._getModuleActivationHandlers(moduleId) + .onActivations.add(serviceIdentifier, onActivation); + } + + public clone(): interfaces.ModuleActivationStore { + const clone = new ModuleActivationStore(); + + this._map.forEach((handlersStore, moduleId) => { + clone._map.set(moduleId, { + onActivations: handlersStore.onActivations.clone(), + onDeactivations: handlersStore.onDeactivations.clone(), + }); + }); + + return clone; + } + + 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: new Lookup(), + onDeactivations: new Lookup() + }; + return handlersStore; + } +} diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index 3a03d3e1..9d219829 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -1,4 +1,10 @@ namespace interfaces { + export type DynamicValue = (context: interfaces.Context) => T | Promise + export type ContainerResolution = T | Promise | (T | Promise)[] + + type AsyncCallback = + TCallback extends (...args: infer TArgs) => infer TResult ? (...args: TArgs) => Promise + : never; export type BindingScope = "Singleton" | "Transient" | "Request"; @@ -42,19 +48,24 @@ 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; + moduleId: ContainerModuleBase["id"]; activated: boolean; serviceIdentifier: ServiceIdentifier; constraint: ConstraintFunction; - dynamicValue: ((context: interfaces.Context) => T) | null; + dynamicValue: DynamicValue | null; scope: BindingScope; type: BindingType; 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; } @@ -165,8 +176,11 @@ 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; + unbindAllAsync(): Promise; isBound(serviceIdentifier: ServiceIdentifier): boolean; isBoundNamed(serviceIdentifier: ServiceIdentifier, named: string | number | symbol): boolean; isBoundTagged(serviceIdentifier: ServiceIdentifier, key: string | number | symbol, value: any): boolean; @@ -176,10 +190,19 @@ 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; - unload(...modules: ContainerModule[]): void; + unload(...modules: ContainerModuleBase[]): void; + unloadAsync(...modules: ContainerModuleBase[]): Promise applyCustomMetadataReader(metadataReader: MetadataReader): void; applyMiddleware(...middleware: Middleware[]): void; snapshot(): void; @@ -193,35 +216,59 @@ 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: Lookup>, + onDeactivations: Lookup> + } + + 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 + } + export type ContainerModuleCallBack = ( 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"] ) => void; - export type AsyncContainerModuleCallBack = ( - bind: interfaces.Bind, - unbind: interfaces.Unbind, - isBound: interfaces.IsBound, - rebind: interfaces.Rebind - ) => Promise; + export type AsyncContainerModuleCallBack = AsyncCallback; export interface ContainerSnapshot { bindings: Lookup>; + activations: Lookup>; + deactivations: Lookup>; middleware: Next | null; + moduleActivationStore: interfaces.ModuleActivationStore; } export interface Lookup extends Clonable> { @@ -229,14 +276,16 @@ 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[]; + removeIntersection(lookup: interfaces.Lookup): void hasKey(serviceIdentifier: ServiceIdentifier): boolean; clone(): Lookup; traverse(func: (key: interfaces.ServiceIdentifier, value: T[]) => void): void; } 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 +320,7 @@ namespace interfaces { to(constructor: new (...args: any[]) => T): BindingInWhenOnSyntax; toSelf(): BindingInWhenOnSyntax; toConstantValue(value: T): BindingWhenOnSyntax; - toDynamicValue(func: (context: Context) => T): BindingInWhenOnSyntax; + toDynamicValue(func: DynamicValue): BindingInWhenOnSyntax; toConstructor(constructor: Newable): BindingWhenOnSyntax; toFactory(factory: FactoryCreator): BindingWhenOnSyntax; toFunction(func: T): BindingWhenOnSyntax; diff --git a/src/inversify.ts b/src/inversify.ts index af502e6d..b23403e5 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"; diff --git a/src/planning/planner.ts b/src/planning/planner.ts index 9ebf6d74..d8fe492f 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/src/resolution/instantiation.ts b/src/resolution/instantiation.ts index 1add363a..6c3b41a1 100644 --- a/src/resolution/instantiation.ts +++ b/src/resolution/instantiation.ts @@ -1,73 +1,147 @@ -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"; import { Metadata } from "../planning/metadata"; +import { isPromise, isPromiseOrContainsPromise } from "../utils/async"; -function _injectProperties( - instance: any, +interface InstanceCreationInstruction{ + constructorInjections: unknown[], + propertyInjections: unknown[], + propertyRequests:interfaces.Request[] +} + +interface ResolvedRequests extends InstanceCreationInstruction{ + isAsync:boolean +} + +interface CreateInstanceWithInjectionArg extends InstanceCreationInstruction{ + constr: interfaces.Newable +} + +function _resolveRequests( childRequests: interfaces.Request[], resolveRequest: interfaces.ResolveRequestHandler -): any { +) : ResolvedRequests { + return childRequests.reduce((resolvedRequests,childRequest)=> { + const injection = resolveRequest(childRequest) + const targetType = childRequest.target.type + if(targetType === TargetTypeEnum.ConstructorArgument){ + resolvedRequests.constructorInjections.push(injection) + }else{ + resolvedRequests.propertyRequests.push(childRequest) + resolvedRequests.propertyInjections.push(injection) + } + if(!resolvedRequests.isAsync){ + resolvedRequests.isAsync = isPromiseOrContainsPromise(injection); + } + return resolvedRequests + },{constructorInjections:[],propertyInjections:[],propertyRequests:[],isAsync:false}) +} + +function _createInstance( + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): T | Promise { + let result: T | Promise; - const propertyInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => - ( - childRequest.target !== null && - childRequest.target.type === TargetTypeEnum.ClassProperty - )); + if (childRequests.length > 0) { + const resolved = _resolveRequests(childRequests,resolveRequest) + const createInstanceWithInjectionsArg: CreateInstanceWithInjectionArg = {...resolved,constr} + if(resolved.isAsync){ + result = createInstanceWithInjectionsAsync(createInstanceWithInjectionsArg) + }else{ + result = createInstanceWithInjections(createInstanceWithInjectionsArg) + } + } else { + result = new constr(); + } - const propertyInjections = propertyInjectionsRequests.map(resolveRequest); + return result; +} - propertyInjectionsRequests.forEach((r: interfaces.Request, index: number) => { - let propertyName = ""; - propertyName = r.target.name.value(); - const injection = propertyInjections[index]; - instance[propertyName] = injection; +function createInstanceWithInjections( + 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 +} - return instance; +async function createInstanceWithInjectionsAsync( + 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){ + if(Array.isArray(injection)){ + injections.push(Promise.all(injection)) + }else{ + injections.push(injection) + } + } + return Promise.all(injections) } -function _createInstance(Func: interfaces.Newable, injections: Object[]): any { - return new Func(...injections); +function _getInstanceAfterPostConstruct(constr: interfaces.Newable, result: T): T | Promise { + + const postConstructResult = _postConstruct(constr, result); + + if (isPromise(postConstructResult)) { + return postConstructResult.then(() => result); + } else { + return result; + } } -function _postConstruct(constr: interfaces.Newable, result: any): void { +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 { - result[data.value](); + return (instance as T & Record void>)[data.value](); } catch (e) { throw new Error(POST_CONSTRUCT_ERROR(constr.name, e.message)); } } } -function resolveInstance( - constr: interfaces.Newable, - childRequests: interfaces.Request[], - resolveRequest: interfaces.ResolveRequestHandler -): any { - - let result: any = null; - - if (childRequests.length > 0) { +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.")); + } - const constructorInjectionsRequests = childRequests.filter((childRequest: interfaces.Request) => - (childRequest.target !== null && childRequest.target.type === TargetTypeEnum.ConstructorArgument)); + if (Reflect.hasMetadata(METADATA_KEY.PRE_DESTROY, constr)) { + throw new Error(PRE_DESTROY_ERROR(constr.name, "Class cannot be instantiated in transient scope.")); + } + } +} - const constructorInjections = constructorInjectionsRequests.map(resolveRequest); +function resolveInstance( + binding: interfaces.Binding, + constr: interfaces.Newable, + childRequests: interfaces.Request[], + resolveRequest: interfaces.ResolveRequestHandler, +): T | Promise { + _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); } - _postConstruct(constr, result); - - return result; } export { resolveInstance }; diff --git a/src/resolution/resolver.ts b/src/resolution/resolver.ts index 9f0e9c8e..ec81241e 100644 --- a/src/resolution/resolver.ts +++ b/src/resolution/resolver.ts @@ -1,6 +1,8 @@ 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"; @@ -25,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); @@ -45,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; @@ -79,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", @@ -100,6 +102,7 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => ); } else if (binding.type === BindingTypeEnum.Instance && binding.implementationType !== null) { result = resolveInstance( + binding, binding.implementationType, childRequests, _resolveRequest(requestScope) @@ -111,15 +114,26 @@ 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); + 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) { binding.cache = result; binding.activated = true; + + if (isPromise(result)) { + result = result.catch((ex) => { + // allow binding to retry in future + binding.cache = null; + binding.activated = false; + + throw ex; + }); + } } if ( @@ -135,9 +149,122 @@ const _resolveRequest = (requestScope: interfaces.RequestScope) => }; -function resolve(context: interfaces.Context): T { +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); + + let container: interfaces.Container; + let containersIteratorResult = containersIterator.next(); + + do { + container = containersIteratorResult.value; + const context = request.parentContext; + const serviceIdentifier = request.serviceIdentifier; + const activationsIterator = _getContainerActivationsForService(container, serviceIdentifier); + + if (isPromise(result)) { + result = _activateContainerAsync(activationsIterator, context, result); + } else { + result = _activateContainer(activationsIterator, context, result); + } + + containersIteratorResult = containersIterator.next(); + + // 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)); + + return result; +} + +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(context, previousResult); + } else { + result = previousResult; + } + + return result; +} + +const _activateContainer = ( + activationsIterator: Iterator>, + context: interfaces.Context, + result: T, +): T | Promise => { + let activation = activationsIterator.next(); + + while (!activation.done) { + result = activation.value(context, result); + + if (isPromise(result)) { + return _activateContainerAsync(activationsIterator, context, result); + } + + activation = activationsIterator.next(); + } + + return result; +} + +const _activateContainerAsync = async( + activationsIterator: Iterator>, + context: interfaces.Context, + resultPromise: Promise, +): Promise => { + let result = await resultPromise + let activation = activationsIterator.next(); + + while (!activation.done) { + result = await activation.value(context, result); + + activation = activationsIterator.next(); + } + + return result; +} + +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>; + + return activations.hasKey(serviceIdentifier) ? activations.get(serviceIdentifier).values() : [].values(); +} + +const _getContainersIterator = (container: interfaces.Container): Iterator => { + const containersStack: interfaces.Container[] = [container]; + + let parent = container.parent; + + while (parent !== null) { + containersStack.push(parent); + + parent = parent.parent; + } + + 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 | 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 }; diff --git a/src/syntax/binding_in_when_on_syntax.ts b/src/syntax/binding_in_when_on_syntax.ts index 5f381a66..b134c17f 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 a885fc76..1cfff586 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: interfaces.BindingActivation): interfaces.BindingWhenSyntax { this._binding.onActivation = handler; return new BindingWhenSyntax(this._binding); } + public onDeactivation(handler: interfaces.BindingDeactivation): interfaces.BindingWhenSyntax { + this._binding.onDeactivation = handler; + return new BindingWhenSyntax(this._binding); + } + } export { BindingOnSyntax }; diff --git a/src/syntax/binding_to_syntax.ts b/src/syntax/binding_to_syntax.ts index 2b9163ff..3dc50878 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/src/syntax/binding_when_on_syntax.ts b/src/syntax/binding_when_on_syntax.ts index 768154d1..83ab76fb 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/src/utils/async.ts b/src/utils/async.ts new file mode 100644 index 00000000..17195235 --- /dev/null +++ b/src/utils/async.ts @@ -0,0 +1,16 @@ +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)[] { + if (isPromise(object)) { + return true; + } + + return Array.isArray(object) && object.some(isPromise); +} + +export { isPromise, isPromiseOrContainsPromise }; diff --git a/src/utils/clonable.ts b/src/utils/clonable.ts new file mode 100644 index 00000000..ae2c1858 --- /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 }; diff --git a/test/container/container.test.ts b/test/container/container.test.ts index a55e9a76..45d93d16 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"; @@ -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"; @@ -127,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", () => { @@ -334,6 +347,50 @@ 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 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 {} @@ -523,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() @@ -896,4 +959,132 @@ 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 { + 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 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 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 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 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); + + }); + + it("should be able to get all the services binded (async)", async () => { + const serviceIdentifier = "service-identifier"; + + const container = new 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, 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 12d2cf0c..3e784a12 100644 --- a/test/container/container_module.test.ts +++ b/test/container/container_module.test.ts @@ -1,4 +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"; @@ -118,4 +120,244 @@ describe("ContainerModule", () => { }); + it("Should be able to add an activation hook through a container module", () => { + + const container = new Container(); + container.bind("A").toDynamicValue(() => "1"); + expect(container.get("A")).to.eql("1"); + + const module = new ContainerModule((bind, unbind, isBound, rebind, unbindAsync, onActivation) => { + bind("B").toConstantValue("2").onActivation(()=>"C"); + onActivation("A", () => "B"); + }); + + container.load(module); + + expect(container.get("A")).to.eql("B"); + expect(container.get("B")).to.eql("C") + }); + + 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, unbindAsync, onActivation, onDeactivation) => { + onDeactivation("A", () => { + deact = true; + }); + }); + + container.load(warriors); + container.get("A"); + container.unbind("A"); + + 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, unBindAsync, onActivation, onDeactivation) => { + onDeactivation("A", async () => { + deact = true; + }); + }); + + container.load(warriors); + container.get("A"); + await container.unbindAsync("A"); + + 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, unbindAsync, 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); + }); + + 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 7d1d4a7f..539c0344 100644 --- a/test/container/lookup.test.ts +++ b/test/container/lookup.test.ts @@ -86,10 +86,28 @@ 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"; - const moduleId2 = "moduleId2"; + const moduleId1 = 1; + const moduleId2 = 2; const warriorId = "Warrior"; const weaponId = "Weapon"; @@ -159,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 lookupToIntersectServiceIdentifier3Values = [31, 32, 33, 34]; + + for (const value of lookupToIntersectServiceIdentifier2Values) { + lookupToIntersect.add(serviceIdentifier2, value); + } + + for (const value of lookupToIntersectServiceIdentifier3Values) { + lookupToIntersect.add(serviceIdentifier3, value); + } + + lookup.removeIntersection(lookupToIntersect); + + expect(lookup.getMap()).to.deep.equal(new Map([ + [serviceIdentifier1, [...serviceIdentifier1Values]], + [serviceIdentifier2, [21, 22]], + ])); + }); + }); diff --git a/test/container/module_activation_store.test.ts b/test/container/module_activation_store.test.ts new file mode 100644 index 00000000..9eb3d4a1 --- /dev/null +++ b/test/container/module_activation_store.test.ts @@ -0,0 +1,115 @@ +import { expect } from "chai"; +import {ModuleActivationStore} from "../../src/container/module_activation_store" +import { interfaces } from "../../src/inversify" + +describe("ModuleActivationStore", () => { + it("should remove handlers added by the module", () => { + const moduleActivationStore = new ModuleActivationStore(); + + 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(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(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(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 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(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(moduleId2, serviceIdentifier1, onActivationMod2); + moduleActivationStore.addDeactivation(moduleId2, serviceIdentifier1, onDeactivationMod2); + + const clone = moduleActivationStore.clone(); + + //change original + const onActivation4: interfaces.BindingActivation = (c,a) => a; + const onDeactivation4: interfaces.BindingDeactivation = (d) => Promise.resolve(); + + moduleActivationStore.addActivation(moduleId1, serviceIdentifier1, onActivation4); + moduleActivationStore.addDeactivation(moduleId1, serviceIdentifier1, onDeactivation4); + moduleActivationStore.remove(moduleId2); + + const cloneModule1Handlers = clone.remove(moduleId1); + + expect(cloneModule1Handlers.onActivations.getMap()).to.deep.equal( + new Map([ + [serviceIdentifier1, [onActivation1, onActivation2]], + [serviceIdentifier2, [onActivation3]] + ]), + ); + + expect(cloneModule1Handlers.onDeactivations.getMap()).to.deep.equal( + new Map([ + [serviceIdentifier1, [onDeactivation1, onDeactivation2]], + [serviceIdentifier2, [onDeactivation3]], + ]), + ); + + const cloneModule2Handlers = clone.remove(moduleId2); + + expect(cloneModule2Handlers.onActivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier1, [onActivationMod2]]]), + ); + + expect(cloneModule2Handlers.onDeactivations.getMap()).to.deep.equal( + new Map([[serviceIdentifier1, [onDeactivationMod2]]]), + ); + }); +}); diff --git a/test/resolution/resolver.test.ts b/test/resolution/resolver.test.ts index f43a12e4..87374772 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"; @@ -17,16 +18,20 @@ 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; beforeEach(() => { - sandbox = sinon.createSandbox(); + sandbox = sinon.createSandbox(); }); afterEach(() => { - sandbox.restore(); + sandbox.restore(); }); it("Should be able to resolve BindingType.Instance bindings", () => { @@ -37,15 +42,15 @@ describe("Resolve", () => { const katanaHandlerId = "KatanaHandler"; const katanaBladeId = "KatanaBlade"; - interface Blade {} + interface Blade { } @injectable() - class KatanaBlade implements Blade {} + class KatanaBlade implements Blade { } - interface Handler {} + interface Handler { } @injectable() - class KatanaHandler implements Handler {} + class KatanaHandler implements Handler { } interface Sword { handler: KatanaHandler; @@ -65,10 +70,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -96,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); @@ -114,15 +119,15 @@ describe("Resolve", () => { const katanaHandlerId = "KatanaHandler"; const katanaBladeId = "KatanaBlade"; - interface Blade {} + interface Blade { } @injectable() - class KatanaBlade implements Blade {} + class KatanaBlade implements Blade { } - interface Handler {} + interface Handler { } @injectable() - class KatanaHandler implements Handler {} + class KatanaHandler implements Handler { } interface Sword { handler: KatanaHandler; @@ -142,10 +147,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -179,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); @@ -192,15 +197,15 @@ describe("Resolve", () => { it("Should throw when an invalid BindingType is detected", () => { - interface Katana {} + interface Katana { } @injectable() - class Katana implements Katana {} + class Katana implements Katana { } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -229,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); @@ -239,15 +244,15 @@ describe("Resolve", () => { it("Should be able to resolve BindingType.ConstantValue bindings", () => { - interface KatanaBlade {} + interface KatanaBlade { } @injectable() - class KatanaBlade implements KatanaBlade {} + class KatanaBlade implements KatanaBlade { } - interface KatanaHandler {} + interface KatanaHandler { } @injectable() - class KatanaHandler implements KatanaHandler {} + class KatanaHandler implements KatanaHandler { } interface Sword { handler: KatanaHandler; @@ -264,10 +269,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -301,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); @@ -315,35 +320,35 @@ describe("Resolve", () => { it("Should be able to resolve BindingType.DynamicValue bindings", () => { - interface UseDate { - doSomething(): Date; - } + 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; - } - } + @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 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); + 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()); + 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); + const subject3 = container.get("UseDate"); + const subject4 = container.get("UseDate"); + expect(subject3.doSomething() === subject4.doSomething()).eql(true); }); @@ -356,15 +361,15 @@ describe("Resolve", () => { const katanaHandlerId = "KatanaHandler"; const katanaBladeId = "KatanaBlade"; - interface KatanaBlade {} + interface KatanaBlade { } @injectable() - class KatanaBlade implements KatanaBlade {} + class KatanaBlade implements KatanaBlade { } - interface KatanaHandler {} + interface KatanaHandler { } @injectable() - class KatanaHandler implements KatanaHandler {} + class KatanaHandler implements KatanaHandler { } interface Sword { handler: KatanaHandler; @@ -384,10 +389,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -414,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); @@ -433,15 +438,15 @@ describe("Resolve", () => { const handlerId = "Handler"; const bladeId = "Blade"; - interface Blade {} + interface Blade { } @injectable() - class KatanaBlade implements Blade {} + class KatanaBlade implements Blade { } - interface Handler {} + interface Handler { } @injectable() - class KatanaHandler implements Handler {} + class KatanaHandler implements Handler { } interface Sword { handler: Handler; @@ -463,10 +468,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -499,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); @@ -518,15 +523,15 @@ describe("Resolve", () => { const katanaHandlerId = "KatanaHandler"; const katanaBladeId = "KatanaBlade"; - interface KatanaBlade {} + interface KatanaBlade { } @injectable() - class KatanaBlade implements KatanaBlade {} + class KatanaBlade implements KatanaBlade { } - interface KatanaHandler {} + interface KatanaHandler { } @injectable() - class KatanaHandler implements KatanaHandler {} + class KatanaHandler implements KatanaHandler { } interface Sword { handler: KatanaHandler; @@ -548,10 +553,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana; @@ -580,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); @@ -601,15 +606,15 @@ describe("Resolve", () => { const handlerId = "Handler"; const bladeId = "Blade"; - interface Blade {} + interface Blade { } @injectable() - class KatanaBlade implements Blade {} + class KatanaBlade implements Blade { } - interface Handler {} + interface Handler { } @injectable() - class KatanaHandler implements Handler {} + class KatanaHandler implements Handler { } interface Sword { handler: Handler; @@ -629,10 +634,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katana: Katana | null; @@ -671,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); @@ -687,13 +692,13 @@ describe("Resolve", () => { it("Should be able to resolve plans with constraints on tagged targets", () => { - interface Weapon {} + interface Weapon { } @injectable() class Katana implements Weapon { } @injectable() - class Shuriken implements Weapon {} + class Shuriken implements Weapon { } interface Warrior { katana: Weapon; @@ -723,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); @@ -733,13 +738,13 @@ describe("Resolve", () => { it("Should be able to resolve plans with constraints on named targets", () => { - interface Weapon {} + interface Weapon { } @injectable() - class Katana implements Weapon {} + class Katana implements Weapon { } @injectable() - class Shuriken implements Weapon {} + class Shuriken implements Weapon { } interface Warrior { katana: Weapon; @@ -751,7 +756,7 @@ describe("Resolve", () => { public katana: Weapon; public shuriken: Weapon; public constructor( - @inject("Weapon") @targetName("katana") @named("strong")katana: Weapon, + @inject("Weapon") @targetName("katana") @named("strong") katana: Weapon, @inject("Weapon") @targetName("shuriken") @named("weak") shuriken: Weapon ) { this.katana = katana; @@ -769,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); @@ -779,13 +784,13 @@ describe("Resolve", () => { it("Should be able to resolve plans with custom contextual constraints", () => { - interface Weapon {} + interface Weapon { } @injectable() - class Katana implements Weapon {} + class Katana implements Weapon { } @injectable() - class Shuriken implements Weapon {} + class Shuriken implements Weapon { } interface Warrior { katana: Weapon; @@ -815,11 +820,11 @@ describe("Resolve", () => { request.target.name.equals("katana")); container.bind(weaponId).to(Shuriken).when((request: interfaces.Request) => - request.target.name.equals("shuriken")); + request.target.name.equals("shuriken")); 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); @@ -869,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); @@ -882,71 +887,166 @@ 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 activation handlers", () => { + it("Should be able to resolve plans with async multi-injections", async () => { - interface Sword { - use(): void; - } + interface Weapon { + name: string; + } - @injectable() - class Katana implements Sword { - public use() { - return "Used Katana!"; - } - } + @injectable() + class Katana implements Weapon { + public name = "Katana"; + } - interface Warrior { - katana: Katana; - } + @injectable() + class Shuriken implements Weapon { + public name = "Shuriken"; + } - @injectable() - class Ninja implements Warrior { - public katana: Katana; - public constructor( - @inject("Katana") katana: Katana - ) { - this.katana = katana; - } + 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 ninjaId = "Ninja"; - const katanaId = "Katana"; + 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 { + 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 container = new Container(); - container.bind(ninjaId).to(Ninja); + const ninjaId = "Ninja"; + const katanaId = "Katana"; - // 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); - 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; - }); + // 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 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); - expect(timeTracker.length).eql(2); + expect(ninja.katana.use()).eql("Used Katana!"); + expect(Array.isArray(timeTracker)).eql(true); + expect(timeTracker.length).eql(2); }); @@ -958,15 +1058,15 @@ describe("Resolve", () => { type KatanaFactory = () => Katana; - interface KatanaBlade {} + interface KatanaBlade { } @injectable() - class KatanaBlade implements KatanaBlade {} + class KatanaBlade implements KatanaBlade { } - interface KatanaHandler {} + interface KatanaHandler { } @injectable() - class KatanaHandler implements KatanaHandler {} + class KatanaHandler implements KatanaHandler { } interface Sword { handler: KatanaHandler; @@ -983,10 +1083,10 @@ describe("Resolve", () => { } } - interface Shuriken {} + interface Shuriken { } @injectable() - class Shuriken implements Shuriken {} + class Shuriken implements Shuriken { } interface Warrior { katanaFactory: KatanaFactory; @@ -1006,7 +1106,7 @@ describe("Resolve", () => { container.bind(ninjaId).to(Ninja); container.bind(shurikenId).to(Shuriken); - const katanaFactoryInstance = function() { + const katanaFactoryInstance = function () { return new Katana(new KatanaHandler(), new KatanaBlade()); }; @@ -1017,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); @@ -1025,6 +1125,7 @@ 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); expect(katanaFactoryBinding.activated).eql(true); @@ -1032,155 +1133,1422 @@ describe("Resolve", () => { 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); + const container = new Container(); + container.bind(ninjaId).to(Ninja); - container.bind(katanaId).to(Katana); + container.bind(katanaId).to(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); + const ninja = resolveTyped(context); - expect(ninja.katana.use()).eql("Used Katana!"); + expect(ninja.katana.use()).eql("Used Katana!"); - }); + }); it("Should throw an error if the @postConstruct method throws an error", () => { - @injectable() - class Katana { + @injectable() + class Katana { - @postConstruct() - public postConstruct() { - throw new Error("Original Message"); - } - } + @postConstruct() + public postConstruct() { + throw new Error("Original Message"); + } + } - expect(resolveInstance.bind(resolveInstance, Katana, [], (request: interfaces.Request) => null)) - .to.throw("@postConstruct error in class Katana: 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; - } + interface Weapon { + use(): string; + } - @injectable() - abstract class Sword implements Weapon { - protected useMessage: string; + @injectable() + abstract class Sword implements Weapon { + protected useMessage: string; - @postConstruct() - public postConstruct () { - this.useMessage = "Used Weapon!"; - } + @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; - } - } + @injectable() + class Katana extends Sword { + public use() { + return this.useMessage; + } + } - 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); + const container = new Container(); + container.bind(ninjaId).to(Ninja); - container.bind(katanaId).to(Katana); + container.bind(katanaId).to(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); + const ninja = resolveTyped(context); - expect(ninja.katana.use()).eql("Used Weapon!"); + 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 ++; - } - } + 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"; + @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 child = parent.createChild(); + const [subject1, subject2] = await Promise.all([ + child.getAsync("Parent"), + child.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(); + let deactivatedDestroyable:Destroyable|null = null + container.bind("Destroyable").toDynamicValue(() => Promise.resolve(new Destroyable())).inSingletonScope() + .onDeactivation((instance) => new Promise((r) => { + 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 () => { + 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"); - 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); + 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 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; + 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 destroy 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 destroy 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 invoke destroy in order (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 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 { + @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 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 { + } + let activated: Constructable | null = null + const container = new Container(); + container.bind("Constructable").toDynamicValue(() => Promise.resolve(new Constructable())).inSingletonScope() + .onActivation((context, c) => new Promise((r) => { + 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 () => { + 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", () => { + 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 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 { + + } + + @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 async 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.`); + }); }); diff --git a/wiki/activation_handler.md b/wiki/activation_handler.md index 91b75c7e..e43ae9dd 100644 --- a/wiki/activation_handler.md +++ b/wiki/activation_handler.md @@ -1,11 +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 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. + +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 +55,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 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/architecture.md b/wiki/architecture.md index 5ba1ae2b..7518b090 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 931b5bbf..20d8f006 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,49 @@ expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Samurai).name).to.eql("S expect(gameContainer.get(JAPAN_EXPANSION_TYPES.Katana).name).to.eql("Katana"); ``` -## container.getNamed() +## 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 + +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(); +container.bind("Weapon").to(Katana); + +let katana = container.get("Weapon"); +``` + +## 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: + +```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\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): T -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 synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -112,9 +166,22 @@ let katana = container.getNamed("Weapon", "japanese"); let shuriken = container.getNamed("Weapon", "chinese"); ``` -## container.getTagged() +## container.getNamedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, named: string | number | symbol): Promise\ -Tagged bindings: +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 = await container.getNamedAsync("Weapon", "japanese"); +let shuriken = await container.getNamedAsync("Weapon", "chinese"); +``` + +## 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 synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -125,9 +192,22 @@ let katana = container.getTagged("Weapon", "faction", "samurai"); let shuriken = container.getTagged("Weapon", "faction", "ninja"); ``` -## container.getAll() +## container.getTaggedAsync\(serviceIdentifier: interfaces.ServiceIdentifier\, key: string | number | symbol, value: any): Promise\ -Get all available bindings for a given identifier: +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 = await container.getTaggedAsync("Weapon", "faction", "samurai"); +let shuriken = await container.getTaggedAsync("Weapon", "faction", "ninja"); +``` + +## container.getAll\(serviceIdentifier: interfaces.ServiceIdentifier\): T[] + +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(); @@ -137,10 +217,21 @@ container.bind("Weapon").to(Shuriken); let weapons = container.getAll("Weapon"); // returns Weapon[] ``` -## container.getAllNamed() +## container.getAllAsync\(serviceIdentifier: interfaces.ServiceIdentifier\): Promise\ -Get all available bindings for a given identifier that match the given -named constraint: +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\(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 synchronously resolved, otherwise an error is thrown: ```ts let container = new Container(); @@ -167,11 +258,39 @@ 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.getAllTagged() +Resolves all the dependencies by its runtime identifier that matches the given named constraint: -Get all available bindings for a given identifier that match 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 = 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 = 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[] + +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(); @@ -198,7 +317,36 @@ expect(es[0].hello).to.eql("hola"); expect(es[1].goodbye).to.eql("adios"); ``` -## container.isBound(serviceIdentifier: ServiceIdentifier) +## 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: + +```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 = 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 = 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: interfaces.ServiceIdentifier\): boolean You can use the `isBound` method to check if there are registered bindings for a given service identifier. @@ -230,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. @@ -255,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. @@ -279,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. @@ -303,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 @@ -334,3 +495,76 @@ 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: interfaces.ServiceIdentifier\, onActivation: interfaces.BindingActivation\): void + +Adds an activation handler for all dependencies registered with the specified 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\): void + +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"); +``` +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 217ba2af..dc356d26 100644 --- a/wiki/container_modules.md +++ b/wiki/container_modules.md @@ -2,6 +2,13 @@ Container modules can help you to manage the complexity of your bindings in very large applications. +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) + ## Synchronous container modules ```ts @@ -14,7 +21,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 +49,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); diff --git a/wiki/deactivation_handler.md b/wiki/deactivation_handler.md new file mode 100644 index 00000000..9dbdc339 --- /dev/null +++ b/wiki/deactivation_handler.md @@ -0,0 +1,81 @@ +# Deactivation handler + +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() +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 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. + +```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/environment.md b/wiki/environment.md index 966bec64..1a3b4a92 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 958fb29e..9b695f3b 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/pre_destroy.md b/wiki/pre_destroy.md new file mode 100644 index 00000000..34a99c4b --- /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, only bindings in singleton scope can contain a method with this decorator. + +```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(); +``` diff --git a/wiki/readme.md b/wiki/readme.md index 29b567a0..6559b1aa 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) diff --git a/wiki/value_injection.md b/wiki/value_injection.md index 7f6ced4b..3e8b8c12 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()); }); ```