Skip to content

Commit

Permalink
Merge 96c3f6f into 1fd35f4
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jan 10, 2019
2 parents 1fd35f4 + 96c3f6f commit 09b3f47
Show file tree
Hide file tree
Showing 23 changed files with 1,250 additions and 125 deletions.
71 changes: 71 additions & 0 deletions docs/site/Context.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,74 @@ class HelloController {
These "sugar" decorators allow you to quickly build up your application without
having to code up all the additional logic by simply giving LoopBack hints (in
the form of metadata) to your intent.

## Context view

Bindings in a context can come and go. It's often desirable for an artifact
(especially an extension point) to keep track of other artifacts (extensions).
For example, the `RestServer` needs to know routes contributed by `controller`
classes or other handlers. Such routes can be added or removed after the
`RestServer` starts. When a controller is added after the application starts,
new routes are bound into the application context. Ideally, the `RestServer`
should be able to pick up these new routes without restarting.

To support the dynamic tracking of such artifacts registered within a context
chain, we introduce `ContextEventListener` interface and `ContextView` class
that can be used to watch a list of bindings matching certain criteria depicted
by a `BindingFilter` function.

```ts
import {Context, ContextView} from '@loopback/context';

// Set up a context chain
const appCtx = new Context('app');
const serverCtx = new Context(appCtx, 'server'); // server -> app

// Define a binding filter to select bindings with tag `controller`
const controllerFilter = binding => binding.tagMap.controller != null;

// Watch for bindings with tag `controller`
const view = serverCtx.watch(controllerFilter);

// No controllers yet
await view.values(); // returns []

// Bind Controller1 to server context
serverCtx
.bind('controllers.Controller1')
.toClass(Controller1)
.tag('controller');

// Resolve to an instance of Controller1
await view.values(); // returns [an instance of Controller1];

// Bind Controller2 to app context
appCtx
.bind('controllers.Controller2')
.toClass(Controller2)
.tag('controller');

// Resolve to an instance of Controller1 and an instance of Controller2
await view.values(); // returns [an instance of Controller1, an instance of Controller2];

// Unbind Controller2
appCtx.unbind('controllers.Controller2');

// No more instance of Controller2
await view.values(); // returns [an instance of Controller1];
```

The key benefit of `ContextView` is that it caches resolved values until context
bindings matching the filter function are added/removed. For most cases, we
don't have to pay the penalty to find/resolve per request.

To fully leverage the live list of extensions, an extension point such as
`RoutingTable` should either keep a pointer to an instance of `ContextView`
corresponding to all `routes` (extensions) in the context chain and use the
`values()` function to match again the live `routes` per request or implement
itself as a `ContextEventListener` to rebuild the routes upon changes of routes
in the context with `listen()`.

If your dependency needs to follow the context for values from bindings matching
a filter, use [`@inject.view`](Decorators_inject.md#@inject.view) for dependency
injection.
108 changes: 98 additions & 10 deletions docs/site/Decorators_inject.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ sidebar: lb4_sidebar
permalink: /doc/en/lb4/Decorators_inject.html
---

## Dependency Injection Decorator
## Dependency Injection Decorators

### @inject

Syntax:
`@inject(bindingSelector: BindingSelector, metadata?: InjectionMetadata)`.

`@inject` is a decorator to annotate class properties or constructor arguments
for automatic injection by LoopBack's IoC container.
Expand Down Expand Up @@ -65,13 +70,27 @@ export class WidgetController {
}
```

The `@inject` decorator now also accepts a binding filter function so that an
array of values can be injected.

```ts
class MyControllerWithValues {
constructor(
@inject(binding => binding.tagNames.includes('foo'))
public values: string[],
) {}
}
```

A few variants of `@inject` are provided to declare special forms of
dependencies:
dependencies.

- `@inject.getter`: inject a getter function that returns a promise of the bound
value of the key
### @inject.getter

Syntax: `@inject.getter(bindingKey: string)`.
`@inject.getter` injects a getter function that returns a promise of the bound
value of the key.

Syntax: `@inject.getter(bindingSelector: BindingSelector)`.

```ts
import {inject, Getter} from '@loopback/context';
Expand All @@ -92,7 +111,19 @@ export class HelloController {
}
```

- `@inject.setter`: inject a setter function to set the bound value of the key
`@inject.getter` also allows the getter function to return an array of values
from bindings that match a filter function.

```ts
class MyControllerWithGetter {
@inject.getter(bindingTagFilter('prime'))
getter: Getter<number[]>;
}
```

### @inject.setter

`@inject.setter` injects a setter function to set the bound value of the key.

Syntax: `@inject.setter(bindingKey: string)`.

Expand All @@ -111,10 +142,12 @@ export class HelloController {
}
```

- `@inject.tag`: inject an array of values by a pattern or regexp to match
binding tags
### @inject.tag

Syntax: `@inject.tag(tag: string | RegExp)`.
`@inject.tag` injects an array of values by a pattern or regexp to match binding
tags.

Syntax: `@inject.tag(tag: BindingTag | RegExp)`.

```ts
class Store {
Expand All @@ -135,7 +168,62 @@ const store = ctx.getSync<Store>('store');
console.log(store.locations); // ['San Francisco', 'San Jose']
```

- `@inject.context`: inject the current context
### @inject.view

`@inject.view` injects a `ContextView` to track a list of bindings matching a
filter function or scope/tags.

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

export class DataSourceTracker {
constructor(
@inject.view(bindingTagFilter('datasource'))
private dataSources: ContextView<DataSource[]>,
) {}

async listDataSources(): Promise<DataSource[]> {
// Use the Getter function to resolve data source instances
return await this.dataSources.values();
}
}
```

The `@inject.view` decorator takes a `BindingFilter` function. It can be applied
to properties in addition to constructor parameters. For example:

```ts
export class DataSourceTracker {
// The target type is `ContextView`
@inject.view(binding => binding.tagMap['datasource'] != null)
private dataSources: ContextView<DataSource>;

async listDataSources(): Promise<DataSource[]> {
// Use the Getter function to resolve data source instances
return await this.dataSources.values();
}

// ...
}
```

Please note that `@inject.view` has two flavors:

- inject a snapshot of values from matching bindings without watching the
context. This is the behavior if the target type is an array instead of Getter
or ContextView.
- inject a Getter/ContextView so that it keeps track of context binding changes.

The resolved value from `@inject.view` injection varies on the target type:

- Function -> a Getter function
- ContextView -> An instance of ContextView
- other -> An array of values resolved from the current state of the context

### @inject.context

`@inject.context` injects the current context.

Syntax: `@inject.context()`.

Expand Down
14 changes: 13 additions & 1 deletion docs/site/Dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ important when testing code interacting with external services like a database
or an OAuth2 provider. Instead of making expensive network requests, the test
can provide a lightweight implementation returning pre-defined responses.

## Configuring what to inject
## Configure what to inject

Now that we write a class that gets the dependencies injected, you are probably
wondering where are these values going to be injected from and how to configure
Expand Down Expand Up @@ -239,6 +239,18 @@ export class MyController {
}
```

## Additional `inject.*` decorators

There are a few special decorators from the `inject` namespace.

- [`@inject.getter`](Decorators_inject.md#@inject.getter)
- [`@inject.setter`](Decorators_inject.md#@inject.setter)
- [`@inject.context`](Decorators_inject.md#@inject.context)
- [`@inject.tag`](Decorators_inject.md#@inject.tag)
- [`@inject.view`](Decorators_inject.md#@inject.view)

See [Inject decorators](Decorators_inject.md) for more details.

## Circular dependencies

LoopBack can detect circular dependencies and report the path which leads to the
Expand Down
71 changes: 71 additions & 0 deletions packages/context/src/binding-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, BindingTag} from './binding';

/**
* A function that filters bindings. It returns `true` to select a given
* binding.
*/
export type BindingFilter<ValueType = unknown> = (
binding: Readonly<Binding<ValueType>>,
) => boolean;

/**
* Create a binding filter for the tag pattern
* @param tagPattern
*/
export function bindingTagFilter(tagPattern: BindingTag | RegExp) {
let bindingFilter: BindingFilter;
if (typeof tagPattern === 'string' || tagPattern instanceof RegExp) {
const regexp =
typeof tagPattern === 'string'
? wildcardToRegExp(tagPattern)
: tagPattern;
bindingFilter = b => Array.from(b.tagNames).some(t => regexp!.test(t));
} else {
bindingFilter = b => {
for (const t in tagPattern) {
// One tag name/value does not match
if (b.tagMap[t] !== tagPattern[t]) return false;
}
// All tag name/value pairs match
return true;
};
}
return bindingFilter;
}

/**
* Create a binding filter from key pattern
* @param keyPattern Binding key, wildcard, or regexp
*/
export function bindingKeyFilter(keyPattern?: string | RegExp) {
let filter: BindingFilter = binding => true;
if (typeof keyPattern === 'string') {
const regex = wildcardToRegExp(keyPattern);
filter = binding => regex.test(binding.key);
} else if (keyPattern instanceof RegExp) {
filter = binding => keyPattern.test(binding.key);
}
return filter;
}

/**
* Convert a wildcard pattern to RegExp
* @param pattern A wildcard string with `*` and `?` as special characters.
* - `*` matches zero or more characters except `.` and `:`
* - `?` matches exactly one character except `.` and `:`
*/
function wildcardToRegExp(pattern: string): RegExp {
// Escape reserved chars for RegExp:
// `- \ ^ $ + . ( ) | { } [ ] :`
let regexp = pattern.replace(/[\-\[\]\/\{\}\(\)\+\.\\\^\$\|\:]/g, '\\$&');
// Replace wildcard chars `*` and `?`
// `*` matches zero or more characters except `.` and `:`
// `?` matches one character except `.` and `:`
regexp = regexp.replace(/\*/g, '[^.:]*').replace(/\?/g, '[^.:]');
return new RegExp(`^${regexp}$`);
}
2 changes: 1 addition & 1 deletion packages/context/src/binding-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export type BindingAddress<T> = string | BindingKey<T>;
export type BindingAddress<T = unknown> = string | BindingKey<T>;

// tslint:disable-next-line:no-unused
export class BindingKey<ValueType> {
Expand Down
1 change: 1 addition & 0 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,4 @@ export class Binding<T = BoundValue> {
return new Binding(key.toString());
}
}

46 changes: 46 additions & 0 deletions packages/context/src/context-listener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding} from './binding';
import {BindingFilter} from './binding-filter';
import {ValueOrPromise} from './value-promise';

/**
* Context event types
*/
export type ContextEventType = 'bind' | 'unbind';

/**
* Listeners of context bind/unbind events
*/
export interface ContextEventListener {
/**
* A filter function to match bindings
*/
filter?: BindingFilter;

/**
* Listen on `bind` or `unbind`
*/
listen(
eventType: ContextEventType,
binding: Readonly<Binding<unknown>>,
): ValueOrPromise<void>;
}

/**
* Subscription of context events. It's modeled after
* https://github.com/tc39/proposal-observable.
*/
export interface Subscription {
/**
* unsubscribe
*/
unsubscribe(): void;
/**
* Is the subscription closed?
*/
closed: boolean;
}

0 comments on commit 09b3f47

Please sign in to comment.