diff --git a/CHANGELOG.md b/CHANGELOG.md index ba30949c..d63f7a7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Add `allowLazyInSync` container option ### Changed diff --git a/src/constants/error_msgs.ts b/src/constants/error_msgs.ts index 9d9b3802..d8ac1c99 100644 --- a/src/constants/error_msgs.ts +++ b/src/constants/error_msgs.ts @@ -17,7 +17,7 @@ export const INVALID_BINDING_TYPE = "Invalid binding type:"; export const NO_MORE_SNAPSHOTS_AVAILABLE = "No snapshot available to restore."; export const INVALID_MIDDLEWARE_RETURN = "Invalid return type in middleware. Middleware must return!"; export const INVALID_FUNCTION_BINDING = "Value provided to function binding must be a function!"; -export const LAZY_IN_SYNC = (key: unknown) => `You are attempting to construct '${key}' in a synchronous way +export const LAZY_IN_SYNC = (key: unknown) => `You are attempting to construct '${TO_STRING(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 " + @@ -41,6 +41,9 @@ 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 CONTAINER_OPTIONS_INVALID_ALLOW_LAZY_IN_SYNC = "Invalid Container option. Allow lazy in sync must" + + "be a boolean"; + export const MULTIPLE_PRE_DESTROY_METHODS = "Cannot apply @preDestroy decorator multiple times in the same class"; export const MULTIPLE_POST_CONSTRUCT_METHODS = "Cannot apply @postConstruct decorator multiple times in the same class"; export const ASYNC_UNBIND_REQUIRED = "Attempting to unbind dependency with asynchronous destruction (@preDestroy or onDeactivation)"; @@ -53,3 +56,5 @@ export const CIRCULAR_DEPENDENCY_IN_FACTORY = (factoryType: string, serviceIdent `service identifier '${serviceIdentifier}'.`; export const STACK_OVERFLOW = "Maximum call stack size exceeded"; + +const TO_STRING = (value: any) => typeof value?.toString === 'function' ? value.toString() : `${value}`; diff --git a/src/container/container.ts b/src/container/container.ts index bea228c1..0696d697 100644 --- a/src/container/container.ts +++ b/src/container/container.ts @@ -65,6 +65,14 @@ class Container implements interfaces.Container { throw new Error(`${ERROR_MSGS.CONTAINER_OPTIONS_MUST_BE_AN_OBJECT}`); } + if (options.allowLazyInSync === undefined) { + options.allowLazyInSync = false; + } else if ( + typeof options.allowLazyInSync !== "boolean" + ) { + throw new Error(ERROR_MSGS.CONTAINER_OPTIONS_INVALID_ALLOW_LAZY_IN_SYNC); + } + if (options.defaultScope === undefined) { options.defaultScope = BindingScopeEnum.Transient; } else if ( @@ -92,9 +100,10 @@ class Container implements interfaces.Container { } this.options = { + allowLazyInSync: options.allowLazyInSync, autoBindInjectable: options.autoBindInjectable, defaultScope: options.defaultScope, - skipBaseClassChecks: options.skipBaseClassChecks + skipBaseClassChecks: options.skipBaseClassChecks, }; this.id = id(); @@ -579,7 +588,7 @@ class Container implements interfaces.Container { ): (T | T[]) { const result = this._get(getArgs); - if (isPromiseOrContainsPromise(result)) { + if (!this.options.allowLazyInSync && isPromiseOrContainsPromise(result)) { throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier)); } diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts index a91c3551..bc9465e3 100644 --- a/src/interfaces/interfaces.ts +++ b/src/interfaces/interfaces.ts @@ -184,6 +184,7 @@ namespace interfaces { autoBindInjectable?: boolean; defaultScope?: BindingScope; skipBaseClassChecks?: boolean; + allowLazyInSync?: boolean; } export interface Container { diff --git a/test/container/container.test.ts b/test/container/container.test.ts index 1329ca70..54e6cd33 100644 --- a/test/container/container.test.ts +++ b/test/container/container.test.ts @@ -687,6 +687,10 @@ describe("Container", () => { const wrong3 = () => new Container(invalidOptions3); expect(wrong3).to.throw(`${ERROR_MSGS.CONTAINER_OPTIONS_INVALID_DEFAULT_SCOPE}`); + const invalidOptions4: any = { allowLazyInSync: "wrongValue" }; + const wrong4 = () => new Container(invalidOptions4); + expect(wrong4).to.throw(ERROR_MSGS.CONTAINER_OPTIONS_INVALID_ALLOW_LAZY_IN_SYNC); + }); it("Should be able to merge two containers", () => { @@ -1170,6 +1174,19 @@ describe("Container", () => { myContainer.resolve(CompositionRoot); // tslint:disable-next-line: no-unused-expression expect(() => myContainer.resolve(CompositionRoot)).not.to.throw; - }) + }); + it("Should throw when synchronously resolving to a promise object", () => { + const identifier = Symbol('PromiseObject') as symbol & interfaces.Abstract>; + const defaultContainer = new Container(); + defaultContainer.bind(identifier).toConstantValue(Promise.resolve({})); + expect(() => defaultContainer.get(identifier)).to.throw(ERROR_MSGS.LAZY_IN_SYNC(identifier)); + }); + + it("Should not throw when allowLazyInSync is set", () => { + const identifier = Symbol('PromiseObject') as symbol & interfaces.Abstract>; + const testContainer = new Container({ allowLazyInSync: true }); + testContainer.bind(identifier).toConstantValue(Promise.resolve({})); + expect(testContainer.get(identifier)).to.be.instanceof(Promise); + }); }); \ No newline at end of file diff --git a/wiki/container_api.md b/wiki/container_api.md index fb825ddf..af695dc3 100644 --- a/wiki/container_api.md +++ b/wiki/container_api.md @@ -13,6 +13,16 @@ You can even provide your own annotation solution. 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. +### allowLazyInSync + +You can use this to skip the promise check when using the synchronous APIs: + +```ts +let container = new Container({ allowLazyInSync: true }); +container.bind(TYPES.AsyncData).toConstantValue(Promise.resolve({ ... })); +container.get(TYPES.AsyncData); // returns the promise without throwing +``` + ### defaultScope The default scope is `transient` when binding to/toSelf/toDynamicValue/toService. @@ -458,7 +468,7 @@ Calls the registration method of each module. See [container modules](https://gi ## container.loadAsync(...modules: interfaces.AsyncContainerModule[]): Promise\ -As per load but for asynchronous registration. +As per load but for asynchronous registration. ## container.rebind\(serviceIdentifier: interfaces.ServiceIdentifier\): : interfaces.BindingToSyntax\