Skip to content

Commit

Permalink
Merge 5afd0a7 into 7be76a4
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Nov 27, 2018
2 parents 7be76a4 + 5afd0a7 commit 405e87c
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 52 deletions.
16 changes: 16 additions & 0 deletions packages/context/src/binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,22 @@ export class Binding<T = BoundValue> {
return this;
}

/**
* Apply a template function to set up the binding with scope, tags.
*
* For example,
* ```ts
* const serverTemplate = (binding: Binding) =>
* binding.inScope(BindingScope.SINGLETON).tag('server');
* binding.apply(serverTemplate);
* ```
* @param template A function to further configure the binding
*/
apply(template: (binding: Binding<T>) => void): this {
template(this);
return this;
}

toJSON(): Object {
// tslint:disable-next-line:no-any
const json: {[name: string]: any} = {
Expand Down
10 changes: 10 additions & 0 deletions packages/context/test/unit/binding.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ describe('Binding', () => {
});
});

describe('apply(templateFunction)', () => {
it('applies a template function', async () => {
binding.apply(b => {
b.inScope(BindingScope.SINGLETON).tag('myTag');
});
expect(binding.scope).to.eql(BindingScope.SINGLETON);
expect(binding.tagNames).to.eql(['myTag']);
});
});

describe('toJSON()', () => {
it('converts a keyed binding to plain JSON object', () => {
const json = binding.toJSON();
Expand Down
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
"copyright.owner": "IBM Corp.",
"license": "MIT",
"dependencies": {
"@loopback/context": "^1.2.0"
"@loopback/context": "^1.2.0",
"debug": "^4.1.0"
},
"devDependencies": {
"@loopback/build": "^1.0.1",
"@loopback/testlab": "^1.0.1",
"@types/debug": "0.0.31",
"@types/node": "^10.11.2"
},
"files": [
Expand Down
107 changes: 81 additions & 26 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,32 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context, Binding, BindingScope, Constructor} from '@loopback/context';
import {Server} from './server';
import {
Binding,
BindingScope,
BindingType,
Constructor,
Context,
} from '@loopback/context';
import {Component, mountComponent} from './component';
import {CoreBindings} from './keys';
import {
asLifeCycleObserverBinding,
isLifeCycleObserver,
isLifeCycleObserverClass,
LifeCycleObserver,
LIFE_CYCLE_OBSERVER_TAG,
} from './lifecycle';
import {Server} from './server';
import debugModule = require('debug');
const debug = debugModule('loopback:core:application');

/**
* Application is the container for various types of artifacts, such as
* components, servers, controllers, repositories, datasources, connectors,
* and models.
*/
export class Application extends Context {
export class Application extends Context implements LifeCycleObserver {
constructor(public options: ApplicationConfig = {}) {
super();

Expand Down Expand Up @@ -41,9 +56,11 @@ export class Application extends Context {
*/
controller(controllerCtor: ControllerClass, name?: string): Binding {
name = name || controllerCtor.name;
return this.bind(`controllers.${name}`)
const key = `controllers.${name}`;
debug('Adding controller %s', name);
return this.bind(key)
.toClass(controllerCtor)
.tag('controller');
.tag(CoreBindings.CONTROLLER_TAG);
}

/**
Expand All @@ -69,10 +86,11 @@ export class Application extends Context {
): Binding {
const suffix = name || ctor.name;
const key = `${CoreBindings.SERVERS}.${suffix}`;
debug('Adding server %s', suffix);
return this.bind(key)
.toClass(ctor)
.tag('server')
.inScope(BindingScope.SINGLETON);
.tag(CoreBindings.SERVER_TAG)
.apply(asLifeCycleObserverBinding);
}

/**
Expand Down Expand Up @@ -110,7 +128,7 @@ export class Application extends Context {
* @memberof Application
*/
public async getServer<T extends Server>(
target: Constructor<T> | String,
target: Constructor<T> | string,
): Promise<T> {
let key: string;
// instanceof check not reliable for string.
Expand All @@ -130,7 +148,17 @@ export class Application extends Context {
* @memberof Application
*/
public async start(): Promise<void> {
await this._forEachServer(s => s.start());
debug('Starting the application...');
const bindings = this._findLifeCycleObserverBindings();
for (const binding of bindings) {
const observer = await this.get<LifeCycleObserver>(binding.key);
if (isLifeCycleObserver(observer)) {
debug('Starting binding %s...', binding.key);
await observer.start();
debug('Binding %s is now started.', binding.key);
}
}
debug('The application is now started.');
}

/**
Expand All @@ -139,25 +167,49 @@ export class Application extends Context {
* @memberof Application
*/
public async stop(): Promise<void> {
await this._forEachServer(s => s.stop());
debug('Stopping the application...');
const bindings = this._findLifeCycleObserverBindings();
// Stop in the reverse order
for (const binding of bindings.reverse()) {
const observer = await this.get<LifeCycleObserver>(binding.key);
if (isLifeCycleObserver(observer)) {
debug('Stopping binding %s...', binding.key);
await observer.stop();
debug('Binding %s is now stopped', binding.key);
}
}
debug('The application is now stopped.');
}

/**
* Helper function for iterating across all registered server components.
* @protected
* @template T
* @param {(s: Server) => Promise<T>} fn The function to run against all
* registered servers
* @memberof Application
* Find all life cycle observer bindings. By default, a constant or singleton
* binding tagged with `LIFE_CYCLE_OBSERVER_TAG` or `CoreBindings.SERVER_TAG`.
*/
protected async _forEachServer<T>(fn: (s: Server) => Promise<T>) {
const bindings = this.find(`${CoreBindings.SERVERS}.*`);
await Promise.all(
bindings.map(async binding => {
const server = await this.get<Server>(binding.key);
return await fn(server);
}),
protected _findLifeCycleObserverBindings() {
const bindings = this.find<LifeCycleObserver>(
binding =>
(binding.type === BindingType.CONSTANT ||
binding.scope === BindingScope.SINGLETON) &&
(binding.tagMap[LIFE_CYCLE_OBSERVER_TAG] ||
binding.tagMap[CoreBindings.SERVER_TAG]),
);
return this._sortLifeCycleObserverBindings(bindings);
}

/**
* Sort the life cycle observer bindings so that we can start/stop them
* in the right order. By default, we can start other observers before servers
* and stop them in the reverse order
* @param bindings Life cycle observer bindings
*/
protected _sortLifeCycleObserverBindings(
bindings: Readonly<Binding<LifeCycleObserver>>[],
) {
return bindings.sort((b1, b2) => {
const tag1 = b1.tagMap[CoreBindings.SERVER_TAG] || '';
const tag2 = b2.tagMap[CoreBindings.SERVER_TAG] || '';
return tag1 > tag2 ? 1 : tag1 < tag2 ? -1 : 0;
});
}

/**
Expand All @@ -183,11 +235,14 @@ export class Application extends Context {
*/
public component(componentCtor: Constructor<Component>, name?: string) {
name = name || componentCtor.name;
const componentKey = `components.${name}`;
this.bind(componentKey)
const componentKey = `${CoreBindings.COMPONENTS}.${name}`;
const binding = this.bind(componentKey)
.toClass(componentCtor)
.inScope(BindingScope.SINGLETON)
.tag('component');
.tag(CoreBindings.COMPONENT_TAG);
if (isLifeCycleObserverClass(componentCtor)) {
binding.apply(asLifeCycleObserverBinding);
}
// Assuming components can be synchronously instantiated
const instance = this.getSync<Component>(componentKey);
mountComponent(this, instance);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {Constructor, Provider, BoundValue, Binding} from '@loopback/context';
import {Server} from './server';
import {Application, ControllerClass} from './application';
import {LifeCycleObserver} from './lifecycle';

/**
* A map of provider classes to be bound to a context
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export * from './server';
export * from './application';
export * from './component';
export * from './keys';
export * from './lifecycle';

// Re-export public Core API coming from dependencies
export * from '@loopback/context';
22 changes: 22 additions & 0 deletions packages/core/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,29 @@ export namespace CoreBindings {
*/
export const SERVERS = 'servers';

/**
* Binding tag for servers
*/
export const SERVER_TAG = 'server';

// component
/**
* Binding key for components
*/
export const COMPONENTS = 'components';

/**
* Binding tag for components
*/
export const COMPONENT_TAG = 'component';

// controller

/**
* Binding tag for components
*/
export const CONTROLLER_TAG = 'controller';

/**
* Binding key for the controller class resolved in the current request
* context
Expand Down
56 changes: 56 additions & 0 deletions packages/core/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/core
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
Constructor,
BindingAddress,
Binding,
BindingScope,
} from '@loopback/context';

/**
* Observers to handle life cycle start/stop events
*/
export interface LifeCycleObserver {
start(): Promise<void> | void;
stop(): Promise<void> | void;
}

/**
* Test if an object implements LifeCycleObserver
* @param obj An object
*/
export function isLifeCycleObserver(obj: {
[name: string]: unknown;
}): obj is LifeCycleObserver {
return typeof obj.start === 'function' && typeof obj.stop === 'function';
}

/**
* Test if a class implements LifeCycleObserver
* @param ctor A class
*/
export function isLifeCycleObserverClass(
ctor: Constructor<unknown>,
): ctor is Constructor<LifeCycleObserver> {
return (
ctor.prototype &&
typeof ctor.prototype.start === 'function' &&
typeof ctor.prototype.stop === 'function'
);
}

/**
* Binding tag for life cycle observers
*/
export const LIFE_CYCLE_OBSERVER_TAG = 'lifeCycleObserver';

/**
* Configure the binding as life cycle observer
* @param binding Binding
*/
export function asLifeCycleObserverBinding<T = unknown>(binding: Binding<T>) {
return binding.tag(LIFE_CYCLE_OBSERVER_TAG).inScope(BindingScope.SINGLETON);
}
13 changes: 3 additions & 10 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {LifeCycleObserver} from './lifecycle';

/**
* Defines the requirements to implement a Server for LoopBack applications:
* start() : Promise<void>
Expand All @@ -15,18 +17,9 @@
* @export
* @interface Server
*/
export interface Server {
export interface Server extends LifeCycleObserver {
/**
* Tells whether the server is listening for connections or not
*/
readonly listening: boolean;

/**
* Start the server
*/
start(): Promise<void>;
/**
* Stop the server
*/
stop(): Promise<void>;
}

0 comments on commit 405e87c

Please sign in to comment.