Skip to content

Commit

Permalink
Merge d03b09b into 181e1f1
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng authored Jan 31, 2019
2 parents 181e1f1 + d03b09b commit 09d1547
Show file tree
Hide file tree
Showing 19 changed files with 1,394 additions and 74 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.
94 changes: 84 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,28 @@ export class WidgetController {
}
```

The `@inject` decorator now also accepts a binding filter function so that an
array of values can be injected. If the target type is not `Array`, an error
will be thrown.

```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.getter`: inject a getter function that returns a promise of the bound
value of the key
`@inject.getter` injects a getter function that returns a promise of the bound
value of the key.

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

```ts
import {inject, Getter} from '@loopback/context';
Expand All @@ -92,7 +112,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(filterByTag('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 +143,12 @@ export class HelloController {
}
```

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

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

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

```ts
class Store {
Expand All @@ -135,7 +169,47 @@ 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 bound values matching
a filter function.

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

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

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

In the example above, `filterByTag` is a helper function that creates a filter
function that matches a given tag. You can define your own filter functions,
such as:

```ts
export class DataSourceTracker {
constructor(
@inject.view(binding => binding.tagNames.includes('datasource'))
private dataSources: ContextView<DataSource[]>,
) {}
}
```

The `@inject.view` decorator takes a `BindingFilter` function. It can only be
applied to a property or method parameter of `ContextView` type.

### @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
1 change: 1 addition & 0 deletions packages/context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@loopback/metadata": "^1.0.5",
"debug": "^4.0.1",
"p-event": "^2.2.0",
"uuid": "^3.2.1"
},
"devDependencies": {
Expand Down
52 changes: 52 additions & 0 deletions packages/context/src/context-observer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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';
import {Context} from './context';

/**
* Context event types. We support `bind` and `unbind` for now but
* keep it open for new types
*/
export type ContextEventType = 'bind' | 'unbind' | string;

/**
* Observers of context bind/unbind events
*/
export interface ContextObserver {
/**
* An optional filter function to match bindings. If not present, the listener
* will be notified of all binding events.
*/
filter?: BindingFilter;

/**
* Listen on `bind`, `unbind`, or other events
* @param eventType Context event type
* @param binding The binding as event source
*/
observe(
eventType: ContextEventType,
binding: Readonly<Binding<unknown>>,
context: Context,
): 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;
}
Loading

0 comments on commit 09d1547

Please sign in to comment.