Skip to content

Commit

Permalink
feat: add app.onStart() and app.onStop() helpers
Browse files Browse the repository at this point in the history
Signed-off-by: Miroslav Bajtoš <[email protected]>
  • Loading branch information
bajtos committed Sep 3, 2020
1 parent c0e5fac commit 92daddd
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 0 deletions.
30 changes: 30 additions & 0 deletions docs/site/Life-cycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,36 @@ export class MyComponentWithObservers implements Component {
}
```

### Shorthand methods

In some cases, it's desirable to register a single function to be called at
start or stop time.

For example, when writing integration-level tests, we can use `app.onStop()` to
register a cleanup routine to be invoked whenever the application is shut down.

```ts
import {Application} from '@loopback/core';

describe('my test suite', () => {
let app: Application;
before(setupApp);
after(() => app.stop());

// the tests come here

async setupApp() {
app = new Application();
app.onStop(async cleanup() {
// do some cleanup
});

await app.boot();
await app.start();
}
});
```

## Discover life cycle observers

The `Application` finds all bindings tagged with `CoreTags.LIFE_CYCLE_OBSERVER`
Expand Down
73 changes: 73 additions & 0 deletions packages/core/src/__tests__/unit/application-lifecycle.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,79 @@ describe('Application life cycle', () => {
expect(stopInvoked).to.be.false(); // not invoked
});
});

describe('app.onStart()', () => {
it('registers the handler as "start" lifecycle observer', async () => {
const app = new Application();
let invoked = false;

const binding = app.onStart(async function doSomething() {
// delay the actual observer code to the next tick to
// verify that the promise returned by an async observer
// is correctly forwarded by LifeCycle wrapper
await Promise.resolve();
invoked = true;
});

expect(binding.key).to.match(/^lifeCycleObservers.doSomething/);

await app.start();
expect(invoked).to.be.true();
});

it('registers multiple handlers with the same name', async () => {
const app = new Application();
const invoked: string[] = [];

app.onStart(() => {
invoked.push('first');
});
app.onStart(() => {
invoked.push('second');
});

await app.start();
expect(invoked).to.deepEqual(['first', 'second']);
});
});

describe('app.onStop()', () => {
it('registers the handler as "stop" lifecycle observer', async () => {
const app = new Application();
let invoked = false;

const binding = app.onStop(async function doSomething() {
// delay the actual observer code to the next tick to
// verify that the promise returned by an async observer
// is correctly forwarded by LifeCycle wrapper
await Promise.resolve();
invoked = true;
});

expect(binding.key).to.match(/^lifeCycleObservers.doSomething/);

await app.start();
expect(invoked).to.be.false();
await app.stop();
expect(invoked).to.be.true();
});

it('registers multiple handlers with the same name', async () => {
const app = new Application();
const invoked: string[] = [];
app.onStop(() => {
invoked.push('first');
});
app.onStop(() => {
invoked.push('second');
});
await app.start();
expect(invoked).to.be.empty();
await app.stop();
// `stop` observers are invoked in reverse order
expect(invoked).to.deepEqual(['second', 'first']);
});
});
});

class ObservingComponentWithServers implements Component, LifeCycleObserver {
Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
JSONObject,
Provider,
registerInterceptor,
ValueOrPromise,
} from '@loopback/context';
import {generateUniqueId} from '@loopback/context/dist/unique-id';
import assert from 'assert';
import debugFactory from 'debug';
import {once} from 'events';
Expand Down Expand Up @@ -312,6 +314,28 @@ export class Application extends Context implements LifeCycleObserver {
this.setState('started');
}

/**
* Register a function to be called when the application starts.
*
* This is a shortcut for adding a binding for a LifeCycleObserver
* implementing a `start()` method.
*
* @param fn The function to invoke, it can be synchronous (returning `void`)
* or asynchronous (returning `Promise<void>`).
* @returns The LifeCycleObserver binding created.
*/
public onStart(fn: () => ValueOrPromise<void>): Binding<LifeCycleObserver> {
const key = [
CoreBindings.LIFE_CYCLE_OBSERVERS,
fn.name || '<onStart>',
generateUniqueId(),
].join('.');

return this.bind<LifeCycleObserver>(key)
.to({start: fn})
.apply(asLifeCycleObserver);
}

/**
* Stop the application instance and all of its registered observers. The
* application state is checked to ensure the integrity of `stop`.
Expand All @@ -335,6 +359,27 @@ export class Application extends Context implements LifeCycleObserver {
this.setState('stopped');
}

/**
* Register a function to be called when the application starts.
*
* This is a shortcut for adding a binding for a LifeCycleObserver
* implementing a `start()` method.
*
* @param fn The function to invoke, it can be synchronous (returning `void`)
* or asynchronous (returning `Promise<void>`).
* @returns The LifeCycleObserver binding created.
*/
public onStop(fn: () => ValueOrPromise<void>): Binding<LifeCycleObserver> {
const key = [
CoreBindings.LIFE_CYCLE_OBSERVERS,
fn.name || '<onStop>',
generateUniqueId(),
].join('.');
return this.bind<LifeCycleObserver>(key)
.to({stop: fn})
.apply(asLifeCycleObserver);
}

private async getLifeCycleObserverRegistry() {
return this.get(CoreBindings.LIFE_CYCLE_OBSERVER_REGISTRY);
}
Expand Down

0 comments on commit 92daddd

Please sign in to comment.