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

feat(context): provide resolution context metadata for factory functions with toDynamicValue() #5370

Merged
merged 2 commits into from
May 15, 2020
Merged
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
23 changes: 21 additions & 2 deletions benchmark/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"test": "lb-mocha \"dist/__tests__/**/*.js\"",
"prestart": "npm run build",
"benchmark:routing": "node ./dist/rest-routing/routing-table",
"benchmark:context": "node ./dist/context-binding/context-binding",
"start": "node ."
},
"author": "IBM Corp.",
Expand All @@ -32,6 +33,7 @@
"!*/__tests__"
],
"dependencies": {
"@loopback/context": "^3.7.0",
"@loopback/example-todo": "^3.3.0",
"@loopback/openapi-spec-builder": "^2.1.3",
"@loopback/rest": "^4.0.0",
Expand All @@ -40,6 +42,7 @@
"@types/request-promise-native": "^1.0.17",
"autocannon": "^4.6.0",
"axios": "^0.19.2",
"benchmark": "^2.1.4",
"byline": "^5.0.0",
"debug": "^4.1.1",
"path-to-regexp": "^6.1.0",
Expand All @@ -49,6 +52,7 @@
"@loopback/build": "^5.3.1",
"@loopback/testlab": "^3.1.3",
"@types/autocannon": "^4.1.0",
"@types/benchmark": "^1.0.31",
"@types/mocha": "^7.0.2",
"@types/node": "^10.17.21",
"mocha": "^7.1.2",
Expand Down
30 changes: 30 additions & 0 deletions benchmark/src/context-binding/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Context binding benchmark

This directory contains a simple benchmarking to measure the performance of
different styles of context bindings.

## Basic use

```sh
npm run -s benchmark:context
```

For example:

```
npm run -s benchmark:context
```

## Base lines

| Test | Ops/sec | Relative margin of error | Runs sampled | Count |
| ------------------------- | --------- | ------------------------ | ------------ | ----- |
| factory - getSync | 1,282,238 | ±1.11% | 93 | 68537 |
| factory - get | 1,222,587 | ±1.19% | 87 | 66558 |
| asyncFactory - get | 362,457 | ±1.78% | 78 | 23284 |
| staticProvider - getSync | 484,494 | ±1.04% | 92 | 26260 |
| staticProvider - get | 475,130 | ±1.13% | 86 | 27435 |
| asyncStaticProvider - get | 339,359 | ±1.43% | 86 | 19046 |
| provider - getSync | 368,127 | ±1.14% | 87 | 21162 |
| provider - get | 366,649 | ±0.86% | 86 | 21358 |
| asyncProvider - get | 291,054 | ±1.94% | 82 | 16557 |
136 changes: 136 additions & 0 deletions benchmark/src/context-binding/context-binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright IBM Corp. 2018,2020. All Rights Reserved.
// Node module: @loopback/benchmark
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context, inject, Provider, ValueFactory} from '@loopback/context';
import Benchmark from 'benchmark';

/**
* Option 1 - use a sync factory function
*/
const factory: ValueFactory = ({context}) => {
const user = context.getSync('user');
return `Hello, ${user}`;
};

/**
* Option 2 - use an async factory function
*/
const asyncFactory: ValueFactory = async ({context}) => {
const user = await context.get('user');
return `Hello, ${user}`;
};

/**
* Option 3 - use a value factory provider class with sync static value() method
* parameter injection
*/
class StaticGreetingProvider {
static value(@inject('user') user: string) {
return `Hello, ${user}`;
}
}

/**
* Option 4 - use a value factory provider class with async static value() method
* parameter injection
*/
class AsyncStaticGreetingProvider {
static value(@inject('user') user: string) {
return Promise.resolve(`Hello, ${user}`);
}
}

/**
* Option 5 - use a regular provider class with sync value()
*/
class GreetingProvider implements Provider<string> {
@inject('user')
private user: string;
raymondfeng marked this conversation as resolved.
Show resolved Hide resolved

value() {
return `Hello, ${this.user}`;
}
}

/**
* Option 6 - use a regular provider class with async value()
*/
class AsyncGreetingProvider implements Provider<string> {
@inject('user')
private user: string;

value() {
return Promise.resolve(`Hello, ${this.user}`);
}
}

setupContextBindings();

function setupContextBindings() {
const ctx = new Context();
ctx.bind('user').to('John');
ctx.bind('greeting.syncFactory').toDynamicValue(factory);
ctx.bind('greeting.asyncFactory').toDynamicValue(asyncFactory);
ctx
.bind('greeting.syncStaticProvider')
.toDynamicValue(StaticGreetingProvider);
ctx
.bind('greeting.asyncStaticProvider')
.toDynamicValue(AsyncStaticGreetingProvider);
ctx.bind('greeting.syncProvider').toProvider(GreetingProvider);
ctx.bind('greeting.asyncProvider').toProvider(AsyncGreetingProvider);
return ctx;
}

function runBenchmark(ctx: Context) {
const options: Benchmark.Options = {
initCount: 1000,
onComplete: (e: Benchmark.Event) => {
const benchmark = e.target as Benchmark;
console.log('%s %d', benchmark, benchmark.count);
},
};
const suite = new Benchmark.Suite('context-bindings');
suite
.add(
'factory - getSync',
() => ctx.getSync('greeting.syncFactory'),
options,
)
.add('factory - get', () => ctx.get('greeting.syncFactory'), options)
.add('asyncFactory - get', () => ctx.get('greeting.asyncFactory'), options)
.add(
'staticProvider - getSync',
() => ctx.getSync('greeting.syncStaticProvider'),
options,
)
.add(
'staticProvider - get',
() => ctx.get('greeting.syncStaticProvider'),
options,
)
.add(
'asyncStaticProvider - get',
() => ctx.get('greeting.asyncStaticProvider'),
options,
)
.add(
'provider - getSync',
() => ctx.getSync('greeting.syncProvider'),
options,
)
.add('provider - get', () => ctx.get('greeting.syncProvider'), options)
.add(
'asyncProvider - get',
() => ctx.get('greeting.asyncProvider'),
options,
)
.run({async: true});
}

if (require.main === module) {
const ctx = setupContextBindings();
runBenchmark(ctx);
}
3 changes: 3 additions & 0 deletions benchmark/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
{
"path": "../examples/todo/tsconfig.json"
},
{
"path": "../packages/context/tsconfig.json"
},
bajtos marked this conversation as resolved.
Show resolved Hide resolved
{
"path": "../packages/openapi-spec-builder/tsconfig.json"
},
Expand Down
44 changes: 44 additions & 0 deletions docs/site/Binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,47 @@ binding.toDynamicValue(() => new Date());
binding.toDynamicValue(() => Promise.resolve('my-value'));
```

The factory function can receive extra information about the context, binding,
and resolution options.

```ts
import {ValueFactory} from '@loopback/context';

// The factory function now have access extra metadata about the resolution
const factory: ValueFactory<string> = resolutionCtx => {
raymondfeng marked this conversation as resolved.
Show resolved Hide resolved
return `Hello, ${resolutionCtx.context.name}#${
resolutionCtx.binding.key
} ${resolutionCtx.options.session?.getBindingPath()}`;
};
const b = ctx.bind('msg').toDynamicValue(factory);
```

bajtos marked this conversation as resolved.
Show resolved Hide resolved
Object destructuring can be used to further simplify a value factory function
that needs to access `context`, `binding`, or `options`.

```ts
const factory: ValueFactory<string> = ({context, binding, options}) => {
return `Hello, ${context.name}#${
binding.key
} ${options.session?.getBindingPath()}`;
};
```

An advanced form of value factory is a class that has a static `value` method
that allows parameter injection.

```ts
import {inject} from '@loopback/context';

class GreetingProvider {
static value(@inject('user') user: string) {
return `Hello, ${user}`;
}
}

const b = ctx.bind('msg').toDynamicValue(GreetingProvider);
```

#### A class

The binding can represent an instance of a class, for example, a controller. A
Expand Down Expand Up @@ -119,6 +160,9 @@ class MyValueProvider implements Provider<string> {
binding.toProvider(MyValueProvider);
```

The provider class serves as the wrapper to declare dependency injections. If
dependency is not needed, `toDynamicValue` can be used instead.

#### An alias

An alias is the key with optional path to resolve the value from another
Expand Down
53 changes: 52 additions & 1 deletion packages/context/src/__tests__/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
inject,
Provider,
} from '../..';
import {ValueFactory} from '../../binding';

const key = 'foo';

Expand Down Expand Up @@ -172,6 +173,56 @@ describe('Binding', () => {
expect(b.type).to.equal(BindingType.DYNAMIC_VALUE);
});

it('support a factory to access context/binding/session', async () => {
const factory: ValueFactory<string> = ({
context,
binding: _binding,
options,
}) => {
return `Hello, ${context.name}#${
_binding.key
} ${options.session?.getBindingPath()}`;
};
const b = ctx.bind('msg').toDynamicValue(factory);
const value = await ctx.get<string>('msg');
expect(value).to.equal('Hello, test#msg msg');
expect(b.type).to.equal(BindingType.DYNAMIC_VALUE);
});

it('supports a factory to use context to look up a binding', async () => {
ctx.bind('user').to('John');
ctx.bind('greeting').toDynamicValue(async ({context}) => {
const user = await context.get('user');
return `Hello, ${user}`;
});
const value = await ctx.get<string>('greeting');
expect(value).to.eql('Hello, John');
});

it('supports a factory to use static provider', () => {
class GreetingProvider {
static value(@inject('user') user: string) {
return `Hello, ${user}`;
}
}
ctx.bind('user').to('John');
ctx.bind('greeting').toDynamicValue(GreetingProvider);
const value = ctx.getSync<string>('greeting');
expect(value).to.eql('Hello, John');
});

it('supports a factory to use async static provider', async () => {
class GreetingProvider {
static async value(@inject('user') user: string) {
return `Hello, ${user}`;
}
}
ctx.bind('user').to('John');
ctx.bind('greeting').toDynamicValue(GreetingProvider);
const value = await ctx.get<string>('greeting');
expect(value).to.eql('Hello, John');
});

it('triggers changed event', () => {
const events = listenOnBinding();
binding.toDynamicValue(() => Promise.resolve('hello'));
Expand Down Expand Up @@ -582,7 +633,7 @@ describe('Binding', () => {
});

function givenBinding() {
ctx = new Context();
ctx = new Context('test');
binding = new Binding(key);
}

Expand Down
Loading