Skip to content

Commit

Permalink
feat(core): introduce basic life cycle support for components
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Nov 5, 2018
1 parent 1bcdb5b commit 6fc3f86
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 16 deletions.
33 changes: 29 additions & 4 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {Context, Binding, BindingScope, Constructor} from '@loopback/context';
import {Server} from './server';
import {Component, mountComponent} from './component';
import {CoreBindings} from './keys';
import {LifeCycleObserver, isLifeCycleObserver} from './lifecycle';

/**
* 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 @@ -130,6 +131,9 @@ export class Application extends Context {
* @memberof Application
*/
public async start(): Promise<void> {
await this._forEachComponent(c => {
if (isLifeCycleObserver(c)) return c.start();
});
await this._forEachServer(s => s.start());
}

Expand All @@ -140,17 +144,20 @@ export class Application extends Context {
*/
public async stop(): Promise<void> {
await this._forEachServer(s => s.stop());
await this._forEachComponent(c => {
if (isLifeCycleObserver(c)) return c.stop();
});
}

/**
* Helper function for iterating across all registered server components.
* Helper function for iterating across all registered servers.
* @protected
* @template T
* @param {(s: Server) => Promise<T>} fn The function to run against all
* registered servers
* @memberof Application
*/
protected async _forEachServer<T>(fn: (s: Server) => Promise<T>) {
protected async _forEachServer<T>(fn: (s: Server) => Promise<T> | T) {
const bindings = this.find(`${CoreBindings.SERVERS}.*`);
await Promise.all(
bindings.map(async binding => {
Expand All @@ -160,6 +167,24 @@ export class Application extends Context {
);
}

/**
* Helper function for iterating across all registered components.
* @protected
* @template T
* @param {(s: Server) => Promise<T>} fn The function to run against all
* registered components
* @memberof Application
*/
protected async _forEachComponent<T>(fn: (c: Component) => Promise<T> | T) {
const bindings = this.find(`${CoreBindings.COMPONENTS}.*`);
await Promise.all(
bindings.map(async binding => {
const component = await this.get<Component>(binding.key);
return await fn(component);
}),
);
}

/**
* Add a component to this application and register extensions such as
* controllers, providers, and servers from the component.
Expand All @@ -183,7 +208,7 @@ export class Application extends Context {
*/
public component(componentCtor: Constructor<Component>, name?: string) {
name = name || componentCtor.name;
const componentKey = `components.${name}`;
const componentKey = `${CoreBindings.COMPONENTS}.${name}`;
this.bind(componentKey)
.toClass(componentCtor)
.inScope(BindingScope.SINGLETON)
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export namespace CoreBindings {
*/
export const SERVERS = 'servers';

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

// controller
/**
* Binding key for the controller class resolved in the current request
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/lifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// 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

/**
* 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';
}
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>;
}
31 changes: 29 additions & 2 deletions packages/core/test/unit/application.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import {Constructor, Context} from '@loopback/context';
import {expect} from '@loopback/testlab';
import {Application, Component, Server} from '../..';
import {LifeCycleObserver} from '../../src/lifecycle';
import {CoreBindings} from '../../src';

describe('Application', () => {
describe('controller binding', () => {
Expand Down Expand Up @@ -94,12 +96,30 @@ describe('Application', () => {
it('starts all injected servers', async () => {
const app = new Application();
app.component(FakeComponent);

const component = await app.get<FakeComponent>(
`${CoreBindings.COMPONENTS}.FakeComponent`,
);
expect(component.status).to.equal('not-initialized');
await app.start();
const server = await app.getServer(FakeServer);

expect(server).to.not.be.null();
expect(server.listening).to.equal(true);
expect(component.status).to.equal('started');
await app.stop();
});

it('starts/stops all registered components', async () => {
const app = new Application();
app.component(FakeComponent);
const component = await app.get<FakeComponent>(
`${CoreBindings.COMPONENTS}.FakeComponent`,
);
expect(component.status).to.equal('not-initialized');
await app.start();
expect(component.status).to.equal('started');
await app.stop();
expect(component.status).to.equal('stopped');
});

it('does not attempt to start poorly named bindings', async () => {
Expand All @@ -118,7 +138,8 @@ describe('Application', () => {
}
});

class FakeComponent implements Component {
class FakeComponent implements Component, LifeCycleObserver {
status = 'not-initialized';
servers: {
[name: string]: Constructor<Server>;
};
Expand All @@ -128,6 +149,12 @@ class FakeComponent implements Component {
FakeServer2: FakeServer,
};
}
start() {
this.status = 'started';
}
stop() {
this.status = 'stopped';
}
}

class FakeServer extends Context implements Server {
Expand Down

0 comments on commit 6fc3f86

Please sign in to comment.