Skip to content

Commit

Permalink
chore(context): address more review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Feb 15, 2019
1 parent 3c49b96 commit 2dac020
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 109 deletions.
123 changes: 70 additions & 53 deletions packages/context/src/__tests__/acceptance/context-view.acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,35 @@ describe('ContextView', () => {
beforeEach(givenViewForControllers);

it('watches matching bindings', async () => {
// We have server: 1, app: 2
expect(await getControllers()).to.eql(['1', '2']);
server.unbind('controllers.1');
// Now we have app: 2
expect(await getControllers()).to.eql(['2']);
app.unbind('controllers.2');
// We have server: ServerController, app: AppController
expect(await getControllers()).to.eql([
'ServerController',
'AppController',
]);
server.unbind('controllers.ServerController');
// Now we have app: AppController
expect(await getControllers()).to.eql(['AppController']);
app.unbind('controllers.AppController');
// All controllers are gone from the context chain
expect(await getControllers()).to.eql([]);
// Add a new controller - server: 3
givenController(server, '3');
expect(await getControllers()).to.eql(['3']);
// Add a new controller - server: AnotherServerController
givenController(server, 'AnotherServerController');
expect(await getControllers()).to.eql(['AnotherServerController']);
});

function givenViewForControllers() {
givenServerWithinAnApp();
contextView = server.createView(filterByTag('controller'));
givenController(server, '1');
givenController(app, '2');
givenController(server, 'ServerController');
givenController(app, 'AppController');
}

function givenController(_ctx: Context, _name: string) {
function givenController(context: Context, name: string) {
class MyController {
name = _name;
name = name;
}
_ctx
.bind(`controllers.${_name}`)
context
.bind(`controllers.${name}`)
.toClass(MyController)
.tag('controller');
}
Expand All @@ -54,23 +57,24 @@ describe('ContextView', () => {
}
});

describe('@inject.* - injects a live collection of matching bindings', async () => {
beforeEach(givenPrimeNumbers);
describe('@inject.* with binding filter', async () => {
const workloadMonitorFilter = filterByTag('workloadMonitor');
beforeEach(givenWorkloadMonitors);

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

class MyControllerWithValues {
constructor(
@inject(filterByTag('prime'))
@inject(workloadMonitorFilter)
public values: number[],
) {}
}

class MyControllerWithView {
@inject.view(filterByTag('prime'))
@inject.view(workloadMonitorFilter)
view: ContextView<number[]>;
}

Expand All @@ -80,7 +84,7 @@ describe('@inject.* - injects a live collection of matching bindings', async ()
const getter = inst.getter;
expect(await getter()).to.eql([3, 5]);
// Add a new binding that matches the filter
givenPrime(server, 7);
givenWorkloadMonitor(server, 'server-reporter-2', 7);
// The getter picks up the new binding
expect(await getter()).to.eql([3, 7, 5]);
});
Expand All @@ -91,56 +95,69 @@ describe('@inject.* - injects a live collection of matching bindings', async ()
expect(inst.values).to.eql([3, 5]);
});

it('injects as values that can be resolved synchronously', () => {
server.bind('my-controller').toClass(MyControllerWithValues);
const inst = server.getSync<MyControllerWithValues>('my-controller');
expect(inst.values).to.eql([3, 5]);
});

it('injects as a view', async () => {
server.bind('my-controller').toClass(MyControllerWithView);
const inst = await server.get<MyControllerWithView>('my-controller');
const view = inst.view;
expect(await view.values()).to.eql([3, 5]);
// Add a new binding that matches the filter
givenPrime(server, 7);
const binding = givenWorkloadMonitor(server, 'server-reporter-2', 7);
// The view picks up the new binding
expect(await view.values()).to.eql([3, 7, 5]);
server.unbind('prime.7');
server.unbind(binding.key);
expect(await view.values()).to.eql([3, 5]);
});

function givenPrimeNumbers() {
function givenWorkloadMonitors() {
givenServerWithinAnApp();
givenPrime(server, 3);
givenPrime(app, 5);
givenWorkloadMonitor(server, 'server-reporter', 3);
givenWorkloadMonitor(app, 'app-reporter', 5);
}

function givenPrime(ctx: Context, p: number) {
ctx
.bind(`prime.${p}`)
.to(p)
.tag('prime');
/**
* Add a workload monitor to the given context
* @param ctx Context object
* @param name Name of the monitor
* @param workload Current workload
*/
function givenWorkloadMonitor(ctx: Context, name: string, workload: number) {
return ctx
.bind(`workloadMonitors.${name}`)
.to(workload)
.tag('workloadMonitor');
}
});

describe('ContextEventListener', () => {
let contextListener: MyListenerForControllers;
beforeEach(givenControllerListener);
describe('ContextEventObserver', () => {
let contextObserver: MyObserverForControllers;
beforeEach(givenControllerObserver);

it('receives notifications of matching binding events', async () => {
const controllers = await getControllers();
// We have server: 1, app: 2
// NOTE: The controllers are not guaranteed to be ['1', '2'] as the events
// are emitted by two context objects and they are processed asynchronously
expect(controllers).to.containEql('1');
expect(controllers).to.containEql('2');
server.unbind('controllers.1');
// Now we have app: 2
expect(await getControllers()).to.eql(['2']);
app.unbind('controllers.2');
// We have server: ServerController, app: AppController
// NOTE: The controllers are not guaranteed to be ['ServerController`,
// 'AppController'] as the events are emitted by two context objects and
// they are processed asynchronously
expect(controllers).to.containEql('ServerController');
expect(controllers).to.containEql('AppController');
server.unbind('controllers.ServerController');
// Now we have app: AppController
expect(await getControllers()).to.eql(['AppController']);
app.unbind('controllers.AppController');
// All controllers are gone from the context chain
expect(await getControllers()).to.eql([]);
// Add a new controller - server: 3
givenController(server, '3');
expect(await getControllers()).to.eql(['3']);
// Add a new controller - server: AnotherServerController
givenController(server, 'AnotherServerController');
expect(await getControllers()).to.eql(['AnotherServerController']);
});

class MyListenerForControllers implements ContextObserver {
class MyObserverForControllers implements ContextObserver {
controllers: Set<string> = new Set();
filter = filterByTag('controller');
observe(event: ContextEventType, binding: Readonly<Binding<unknown>>) {
Expand All @@ -152,12 +169,12 @@ describe('ContextEventListener', () => {
}
}

function givenControllerListener() {
function givenControllerObserver() {
givenServerWithinAnApp();
contextListener = new MyListenerForControllers();
server.subscribe(contextListener);
givenController(server, '1');
givenController(app, '2');
contextObserver = new MyObserverForControllers();
server.subscribe(contextObserver);
givenController(server, 'ServerController');
givenController(app, 'AppController');
}

function givenController(ctx: Context, controllerName: string) {
Expand All @@ -173,7 +190,7 @@ describe('ContextEventListener', () => {
async function getControllers() {
return new Promise<string[]>(resolve => {
// Wrap it inside `setImmediate` to make the events are triggered
setImmediate(() => resolve(Array.from(contextListener.controllers)));
setImmediate(() => resolve(Array.from(contextObserver.controllers)));
});
}
});
Expand Down
18 changes: 18 additions & 0 deletions packages/context/src/binding-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// License text available at https://opensource.org/licenses/MIT

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

/**
* A function that filters bindings. It returns `true` to select a given
Expand All @@ -13,6 +14,23 @@ export type BindingFilter<ValueType = unknown> = (
binding: Readonly<Binding<ValueType>>,
) => boolean;

/**
* Select binding(s) by key or a filter function
*/
export type BindingSelector<ValueType = unknown> =
| BindingAddress<ValueType>
| BindingFilter<ValueType>;

/**
* Type guard for binding address
* @param bindingSelector
*/
export function isBindingAddress(
bindingSelector: BindingSelector,
): bindingSelector is BindingAddress {
return typeof bindingSelector !== 'function';
}

/**
* Create a binding filter for the tag pattern
* @param tagPattern Binding tag name, regexp, or object
Expand Down
24 changes: 14 additions & 10 deletions packages/context/src/context-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import {Binding} from './binding';
import {BindingFilter} from './binding-filter';
import {Context} from './context';
import {
ContextObserver,
ContextEventType,
ContextObserver,
Subscription,
} from './context-observer';
import {Getter} from './inject';
import {ResolutionSession} from './resolution-session';
import {resolveList, ValueOrPromise} from './value-promise';
import {isPromiseLike, resolveList, ValueOrPromise} from './value-promise';
const debug = debugFactory('loopback:context:view');
const nextTick = promisify(process.nextTick);

Expand Down Expand Up @@ -100,32 +100,36 @@ export class ContextView<T = unknown> implements ContextObserver {
*/
resolve(session?: ResolutionSession): ValueOrPromise<T[]> {
debug('Resolving values');
// We don't cache values with this method as it returns `ValueOrPromise`
// for inject `resolve` function to allow `getSync` but `this._cachedValues`
// expects `T[]`
return resolveList(this.bindings, b => {
if (this._cachedValues != null) return this._cachedValues;
let result = resolveList(this.bindings, b => {
return b.getValue(this.context, ResolutionSession.fork(session));
});
if (isPromiseLike(result)) {
result = result.then(values => (this._cachedValues = values));
} else {
this._cachedValues = result;
}
return result;
}

/**
* Get the list of resolved values. If they are not cached, it tries to find
* and resolve them.
*/
async values(): Promise<T[]> {
async values(session?: ResolutionSession): Promise<T[]> {
debug('Reading values');
// Wait for the next tick so that context event notification can be emitted
await nextTick();
if (this._cachedValues == null) {
this._cachedValues = await this.resolve();
this._cachedValues = await this.resolve(session);
}
return this._cachedValues;
}

/**
* As a `Getter` function
*/
asGetter(): Getter<T[]> {
return () => this.values();
asGetter(session?: ResolutionSession): Getter<T[]> {
return () => this.values(session);
}
}
Loading

0 comments on commit 2dac020

Please sign in to comment.