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

Improve async cache #1328

Merged
merged 10 commits into from
Jun 8, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- @postConstruct can target an asyncronous function #1132
- Singleton scoped services cache resolved values once the result promise is fulfilled #1320

## [5.1.1] - 2021-04-25
-Fix pre-publish for build artifacts
Expand Down
65 changes: 47 additions & 18 deletions src/scope/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,58 @@ export const tryGetFromScope = <T>(
export const saveToScope = <T>(
requestScope: interfaces.RequestScope,
binding:interfaces.Binding<T>,
result:T | Promise<T>
result: T | Promise<T>
): void => {
// store in cache if scope is singleton
if (binding.scope === BindingScopeEnum.Singleton) {
binding.cache = result;
binding.activated = true;

if (isPromise(result)) {
result.catch((ex) => {
// allow binding to retry in future
binding.cache = null;
binding.activated = false;

throw ex;
});
}
_saveToSingletonScope(binding, result);
}

if (
binding.scope === BindingScopeEnum.Request &&
requestScope !== null &&
!requestScope.has(binding.id)
binding.scope === BindingScopeEnum.Request
) {
_saveToRequestScope(requestScope, binding, result);
}
}

const _saveToRequestScope = <T>(
requestScope: interfaces.RequestScope,
binding:interfaces.Binding<T>,
result: T | Promise<T>
): void => {
if (
requestScope !== null &&
!requestScope.has(binding.id)
) {
requestScope.set(binding.id, result);
requestScope.set(binding.id, result);
}
}

const _saveToSingletonScope = <T>(
binding:interfaces.Binding<T>,
result: T | Promise<T>
): void => {
// store in cache if scope is singleton
binding.cache = result;
binding.activated = true;

if (isPromise(result)) {
void _saveAsyncResultToSingletonScope(binding, result);
}
}

const _saveAsyncResultToSingletonScope = async <T>(
binding:interfaces.Binding<T>,
asyncResult: Promise<T>
): Promise<void> => {
try {
const result = await asyncResult;

binding.cache = result;
} catch (ex: unknown) {
// allow binding to retry in future
binding.cache = null;
binding.activated = false;

throw ex;
}
}
25 changes: 25 additions & 0 deletions test/resolution/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2558,4 +2558,29 @@ describe("Resolve", () => {
expect(() => container.get<string>("async")).to.throw(`You are attempting to construct 'async' in a synchronous way
but it has asynchronous dependencies.`);
});

it("Should cache a a resolved value on singleton when possible", async () => {
const container = new Container();

const asyncServiceIdentifier = "async";

const asyncServiceDynamicResolvedValue = "foobar";
const asyncServiceDynamicValue = Promise.resolve(asyncServiceDynamicResolvedValue);
const asyncServiceDynamicValueCallback = sinon.spy(() => asyncServiceDynamicValue);

container
.bind<string>(asyncServiceIdentifier)
.toDynamicValue(asyncServiceDynamicValueCallback)
.inSingletonScope();

const serviceFromGetAsync = await container.getAsync(asyncServiceIdentifier);

await asyncServiceDynamicValue;

const serviceFromGet = container.get(asyncServiceIdentifier);

expect(asyncServiceDynamicValueCallback.callCount).to.eq(1);
expect(serviceFromGetAsync).eql(asyncServiceDynamicResolvedValue);
expect(serviceFromGet).eql(asyncServiceDynamicResolvedValue);
});
});
8 changes: 8 additions & 0 deletions wiki/scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ In terms of how scope behaves we can group these types of bindings in two main g
- Bindings that will inject an `object`
- Bindings that will inject a `function`

Last but not least, those bindings can inject a value or a promise to a value. There are some caveats regarding the injection of a promise to a value:

- Bindings that will inject a `Promise`

### Bindings that will inject an `object`

In this group are included the following types of binding:
Expand Down Expand Up @@ -80,6 +84,10 @@ container.bind<Katana>("Katana").to(Katana).inTransientScope();
container.bind<Katana>("Katana").to(Katana).inSingletonScope();
```

### Bindings that will inject a `Promise`

- When injecting a promise to a value, the container firstly caches the promise itself the first time a user tries to get the service. Once the promise is fulfilled, the container caches the resolved value instead in order to allow users to get the service syncronously.

## About `inRequestScope`

When we use inRequestScope we are using a special kind of singleton.
Expand Down