Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allowLazyInSync container option #1461

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion src/constants/error_msgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 " +
Expand All @@ -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)";
Expand All @@ -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}`;
13 changes: 11 additions & 2 deletions src/container/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -579,7 +588,7 @@ class Container implements interfaces.Container {
): (T | T[]) {
const result = this._get<T>(getArgs);

if (isPromiseOrContainsPromise<T>(result)) {
if (!this.options.allowLazyInSync && isPromiseOrContainsPromise<T>(result)) {
throw new Error(ERROR_MSGS.LAZY_IN_SYNC(getArgs.serviceIdentifier));
Copy link
Contributor Author

@paul-marechal paul-marechal May 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getArgs.serviceIdentifier can be a symbol here, and it fails to get converted to string in LAZY_IN_SYNC.

I added a quick fix in error_msgs.ts to not fail in such a case. Not sure if the impl is good:

const TO_STRING = (value: any) => typeof value?.toString === 'function' ? value.toString() : `${value}`;

Surprisingly, doing the following fails:

const symbol = Symbol('test');
console.log(`${symbol}`);

But calling .toString() manually on the symbol works... Gotta love working with JavaScript :)

}

Expand Down
1 change: 1 addition & 0 deletions src/interfaces/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ namespace interfaces {
autoBindInjectable?: boolean;
defaultScope?: BindingScope;
skipBaseClassChecks?: boolean;
allowLazyInSync?: boolean;
}

export interface Container {
Expand Down
19 changes: 18 additions & 1 deletion test/container/container.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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<Promise<object>>;
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<Promise<object>>;
const testContainer = new Container({ allowLazyInSync: true });
testContainer.bind(identifier).toConstantValue(Promise.resolve({}));
expect(testContainer.get(identifier)).to.be.instanceof(Promise);
});
});
12 changes: 11 additions & 1 deletion wiki/container_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -458,7 +468,7 @@ Calls the registration method of each module. See [container modules](https://gi

## container.loadAsync(...modules: interfaces.AsyncContainerModule[]): Promise\<void>

As per load but for asynchronous registration.
As per load but for asynchronous registration.

## container.rebind\<T>(serviceIdentifier: interfaces.ServiceIdentifier\<T>): : interfaces.BindingToSyntax\<T>

Expand Down