Skip to content

Commit

Permalink
feat(core): enable start/stop/boot to be idempotent
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Dec 3, 2019
1 parent 5edf46d commit b614a78
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 14 deletions.
7 changes: 3 additions & 4 deletions packages/boot/src/__tests__/unit/bootstrapper.unit.ts
Expand Up @@ -80,13 +80,12 @@ describe('boot-strapper unit tests', () => {
expect(app.state).to.equal('stopped');
});

it('throws error with in-process application states', async () => {
it('awaits booted if the application is booting', async () => {
const boot = app.boot();
expect(app.state).to.eql('booting');
await expect(app.boot()).to.be.rejectedWith(
/Cannot boot the application as it is booting\./,
);
const bootAgain = app.boot();
await boot;
await bootAgain;
expect(app.state).to.eql('booted');
});

Expand Down
1 change: 1 addition & 0 deletions packages/boot/src/mixins/boot.mixin.ts
Expand Up @@ -65,6 +65,7 @@ export function BootMixin<T extends Constructor<any>>(superClass: T) {
* Convenience method to call bootstrapper.boot() by resolving bootstrapper
*/
async boot(): Promise<void> {
if (this.state === 'booting') return this.awaitState('booted');
this.assertNotInProcess('boot');
this.assertInStates('boot', 'created', 'booted');
if (this.state === 'booted') return;
Expand Down
21 changes: 21 additions & 0 deletions packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/core/package.json
Expand Up @@ -20,7 +20,8 @@
"license": "MIT",
"dependencies": {
"@loopback/context": "^1.24.0",
"debug": "^4.1.1"
"debug": "^4.1.1",
"p-event": "^4.1.0"
},
"devDependencies": {
"@loopback/build": "^2.1.0",
Expand Down
34 changes: 26 additions & 8 deletions packages/core/src/__tests__/unit/application-lifecycle.unit.ts
Expand Up @@ -61,29 +61,47 @@ describe('Application life cycle', () => {
]);
});

it('emits state events', async () => {
const app = new Application();
const events: string[] = [];
for (const e of ['starting', 'started', 'stopping', 'stopped']) {
app.on(e, event => {
events.push(e);
});
}
const start = app.start();
expect(events).to.eql(['starting']);
await start;
expect(events).to.eql(['starting', 'started']);
const stop = app.stop();
expect(events).to.eql(['starting', 'started', 'stopping']);
await stop;
expect(events).to.eql(['starting', 'started', 'stopping', 'stopped']);
});

it('allows application.stop when it is created', async () => {
const app = new Application();
await app.stop(); // no-op
expect(app.state).to.equal('created');
});

it('rejects application.stop when it is stopping', async () => {
it('await application.stop when it is stopping', async () => {
const app = new Application();
await app.start();
const stop = app.stop();
await expect(app.stop()).to.be.rejectedWith(
/Cannot stop the application as it is stopping\./,
);
const stopAgain = app.stop();
await stop;
await stopAgain;
expect(app.state).to.equal('stopped');
});

it('rejects application.start when it is not created or stopped', async () => {
it('await application.start when it is starting', async () => {
const app = new Application();
const start = app.start();
await expect(app.start()).to.be.rejectedWith(
/Cannot start the application as it is starting\./,
);
const startAgain = app.start();
await start;
await startAgain;
expect(app.state).to.equal('started');
});
});

Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/application.ts
Expand Up @@ -13,6 +13,7 @@ import {
} from '@loopback/context';
import * as assert from 'assert';
import * as debugFactory from 'debug';
import pEvent from 'p-event';
import {Component, mountComponent} from './component';
import {CoreBindings, CoreTags} from './keys';
import {
Expand Down Expand Up @@ -246,7 +247,14 @@ export class Application extends Context implements LifeCycleObserver {
protected setState(state: string) {
const oldState = this._state;
this._state = state;
this.emit('stateChanged', {from: oldState, to: this._state});
if (oldState !== state) {
this.emit('stateChanged', {from: oldState, to: this._state});
this.emit(state);
}
}

protected async awaitState(state: string) {
await pEvent(this, state);
}

/**
Expand All @@ -256,6 +264,7 @@ export class Application extends Context implements LifeCycleObserver {
* If the application is already started, no operation is performed.
*/
public async start(): Promise<void> {
if (this._state === 'starting') return this.awaitState('started');
this.assertNotInProcess('start');
// No-op if it's started
if (this._state === 'started') return;
Expand All @@ -273,6 +282,7 @@ export class Application extends Context implements LifeCycleObserver {
* performed.
*/
public async stop(): Promise<void> {
if (this._state === 'stopping') return this.awaitState('stopped');
this.assertNotInProcess('stop');
// No-op if it's created or stopped
if (this._state !== 'started') return;
Expand Down

0 comments on commit b614a78

Please sign in to comment.