diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index 75693f44c8a1..b635204b2d53 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -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(); @@ -130,6 +131,9 @@ export class Application extends Context { * @memberof Application */ public async start(): Promise { + await this._forEachComponent(c => { + if (isLifeCycleObserver(c)) return c.start(); + }); await this._forEachServer(s => s.start()); } @@ -140,17 +144,20 @@ export class Application extends Context { */ public async stop(): Promise { 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} fn The function to run against all * registered servers * @memberof Application */ - protected async _forEachServer(fn: (s: Server) => Promise) { + protected async _forEachServer(fn: (s: Server) => Promise | T) { const bindings = this.find(`${CoreBindings.SERVERS}.*`); await Promise.all( bindings.map(async binding => { @@ -160,6 +167,24 @@ export class Application extends Context { ); } + /** + * Helper function for iterating across all registered components. + * @protected + * @template T + * @param {(s: Server) => Promise} fn The function to run against all + * registered components + * @memberof Application + */ + protected async _forEachComponent(fn: (c: Component) => Promise | T) { + const bindings = this.find(`${CoreBindings.COMPONENTS}.*`); + await Promise.all( + bindings.map(async binding => { + const component = await this.get(binding.key); + return await fn(component); + }), + ); + } + /** * Add a component to this application and register extensions such as * controllers, providers, and servers from the component. @@ -183,7 +208,7 @@ export class Application extends Context { */ public component(componentCtor: Constructor, name?: string) { name = name || componentCtor.name; - const componentKey = `components.${name}`; + const componentKey = `${CoreBindings.COMPONENTS}.${name}`; this.bind(componentKey) .toClass(componentCtor) .inScope(BindingScope.SINGLETON) diff --git a/packages/core/src/keys.ts b/packages/core/src/keys.ts index 847388fc339c..c0960f82e287 100644 --- a/packages/core/src/keys.ts +++ b/packages/core/src/keys.ts @@ -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 diff --git a/packages/core/src/lifecycle.ts b/packages/core/src/lifecycle.ts new file mode 100644 index 000000000000..2e2e77d69b74 --- /dev/null +++ b/packages/core/src/lifecycle.ts @@ -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; + stop(): Promise | 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'; +} diff --git a/packages/core/src/server.ts b/packages/core/src/server.ts index 771abc575861..0fa696d58446 100644 --- a/packages/core/src/server.ts +++ b/packages/core/src/server.ts @@ -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 @@ -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; - /** - * Stop the server - */ - stop(): Promise; } diff --git a/packages/core/test/unit/application.unit.ts b/packages/core/test/unit/application.unit.ts index 7bf921875ecc..0699f2162a7c 100644 --- a/packages/core/test/unit/application.unit.ts +++ b/packages/core/test/unit/application.unit.ts @@ -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', () => { @@ -94,12 +96,30 @@ describe('Application', () => { it('starts all injected servers', async () => { const app = new Application(); app.component(FakeComponent); - + const component = await app.get( + `${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( + `${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 () => { @@ -118,7 +138,8 @@ describe('Application', () => { } }); -class FakeComponent implements Component { +class FakeComponent implements Component, LifeCycleObserver { + status = 'not-initialized'; servers: { [name: string]: Constructor; }; @@ -128,6 +149,12 @@ class FakeComponent implements Component { FakeServer2: FakeServer, }; } + start() { + this.status = 'started'; + } + stop() { + this.status = 'stopped'; + } } class FakeServer extends Context implements Server {