Skip to content

Commit 27c8127

Browse files
committed
feat(core): introduce life cycle support
1 parent 24545f5 commit 27c8127

15 files changed

+1184
-80
lines changed

docs/site/Life-cycle.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
---
2+
lang: en
3+
title: 'Life cycle events and observers'
4+
keywords: LoopBack 4.0, LoopBack 4
5+
sidebar: lb4_sidebar
6+
permalink: /doc/en/lb4/Life-cycle.html
7+
---
8+
9+
## Overview
10+
11+
A LoopBack application has its own life cycles at runtime. There are two methods
12+
to control the transition of states of `Application`.
13+
14+
- start(): Start the application
15+
- stop(): Stop the application
16+
17+
It's often desirable for various types of artifacts to participate in the life
18+
cycles and perform related processing upon `start` and `stop`. Good examples of
19+
such artifacts are:
20+
21+
- Servers
22+
23+
- start: Starts the HTTP server listening for connections.
24+
- stop: Stops the server from accepting new connections.
25+
26+
- Components
27+
28+
- A component can register life cycle observers
29+
30+
- DataSources
31+
32+
- connect: Connect to the underlying database or service
33+
- disconnect: Disconnect from the underlying database or service
34+
35+
- Custom scripts
36+
- start: Custom logic to be invoked when the application starts
37+
- stop: Custom logic to be invoked when the application stops
38+
39+
## The `LifeCycleObserver` interface
40+
41+
To react on life cycle events, a life cycle observer implements the
42+
`LifeCycleObserver` interface.
43+
44+
```ts
45+
import {ValueOrPromise} from '@loopback/context';
46+
47+
/**
48+
* Observers to handle life cycle start/stop events
49+
*/
50+
export interface LifeCycleObserver {
51+
start?(): ValueOrPromise<void>;
52+
stop?(): ValueOrPromise<void>;
53+
}
54+
```
55+
56+
Please note all methods are optional so that an observer can opt in certain
57+
events. Each main events such as `start` and `stop` are further divided into
58+
three sub-phases to allow the multiple-step processing.
59+
60+
## Register a life cycle observer
61+
62+
A life cycle observer can be registered by calling `lifeCycleObserver()` of the
63+
application. It binds the observer to the application context with a special
64+
tag - `CoreTags.LIFE_CYCLE_OBSERVER`.
65+
66+
```ts
67+
app.lifeCycleObserver(MyObserver);
68+
```
69+
70+
Please note that `app.server()` automatically registers servers as life cycle
71+
observers.
72+
73+
Life cycle observers can be registered via a component too:
74+
75+
```ts
76+
export class MyComponentWithObservers {
77+
lifeCycleObservers: [XObserver, YObserver];
78+
}
79+
```
80+
81+
## Discover life cycle observers
82+
83+
The `Application` finds all bindings tagged with `CoreTags.LIFE_CYCLE_OBSERVER`
84+
within the context chain and resolve them as observers to be notified.
85+
86+
## Notify life cycle observers of start/stop related events by order
87+
88+
There may be dependencies between life cycle observers and their order of
89+
processing for `start` and `stop` need to be coordinated. For example, we
90+
usually start a server to listen on incoming requests only after other parts of
91+
the application are ready to handle requests. The stop sequence is typically
92+
processed in the reverse order. To support such cases, we introduce
93+
two-dimension steps to control the order of life cycle actions.
94+
95+
### Observer groups
96+
97+
First of all, we allow each of the life cycle observers to be tagged with a
98+
group. For example:
99+
100+
- datasource
101+
102+
- connect/disconnect
103+
- mongodb
104+
- mysql
105+
106+
- server
107+
- rest
108+
- gRPC
109+
110+
We can then configure the application to trigger observers group by group as
111+
configured by an array of groups in order such as `['datasource', 'server']`.
112+
Observers within the same group can be notified in parallel.
113+
114+
For example,
115+
116+
```ts
117+
app
118+
.bind('observers.MyObserver')
119+
.toClass(MyObserver)
120+
.tag({
121+
[CoreTags.LIFE_CYCLE_OBSERVER_GROUP]: 'g1',
122+
})
123+
.apply(asLifeCycleObserverBinding);
124+
```
125+
126+
The observer class can also be decorated with `@bind` to provide binding
127+
metadata.
128+
129+
```ts
130+
@bind(
131+
{
132+
tags: {
133+
[CoreTags.LIFE_CYCLE_OBSERVER_GROUP]: 'g1',
134+
},
135+
},
136+
asLifeCycleObserverBinding,
137+
)
138+
export class MyObserver {
139+
// ...
140+
}
141+
142+
app.add(createBindingFromClass(MyObserver));
143+
```
144+
145+
The order of observers are controlled by a `groupsByOrder` property of
146+
`LifeCycleObserverRegistry`, which receives its options including the
147+
`groupsByOrder` from `CoreBindings.LIFE_CYCLE_OBSERVER_OPTIONS`. Thus the
148+
initial `groupsByOrder` can be set as follows:
149+
150+
```ts
151+
app
152+
.bind(CoreBindings.LIFE_CYCLE_OBSERVER_OPTIONS)
153+
.to({groupsByOrder: ['g1', 'g2', 'server']});
154+
```
155+
156+
Or:
157+
158+
```ts
159+
const registry = await app.get(CoreBindings.LIFE_CYCLE_OBSERVER_REGISTRY);
160+
registry.setGroupsByOrder(['g1', 'g2', 'server']);
161+
```
162+
163+
Observers are sorted using `groupsByOrder` as the relative order. If an observer
164+
is tagged with a group that are not in `groupsByOrder`, it will come before any
165+
groups within `groupsByOrder`. Such custom groups are also sorted by their names
166+
alphabetically.
167+
168+
In the example below, `groupsByOrder` is set to `['g1', 'g2']`. Given the
169+
following observers:
170+
171+
- 'my-observer-1' ('g1')
172+
- 'my-observer-2' ('g2')
173+
- 'my-observer-4' ('2-custom-group')
174+
- 'my-observer-3' ('1-custom-group')
175+
176+
The sorted observer groups will be:
177+
178+
```ts
179+
{
180+
'1-custom-group': ['my-observer-3'],
181+
'2-custom-group': ['my-observer-4'],
182+
'g1': ['my-observer-1'],
183+
'g2': ['my-observer-2'],
184+
}
185+
```
186+
187+
### Event phases
188+
189+
It's also desirable for certain observers to do some processing before, upon, or
190+
after the `start` and `stop` events. To allow that, we notify each observer in
191+
three phases:
192+
193+
- start: preStart, start, and postStart
194+
- stop: preStop, stop, and postStop
195+
196+
Combining groups and event phases, it's flexible to manage multiple observers so
197+
that they can be started/stopped gracefully in order.
198+
199+
For example, with a group order as `['datasource', 'server']` and three
200+
observers registered as follows:
201+
202+
- datasource group: MySQLDataSource, MongoDBDataSource
203+
- server group: RestServer
204+
205+
The start sequence will be:
206+
207+
1. MySQLDataSource.preStart
208+
2. MongoDBDataSource.preStart
209+
3. RestServer.preStart
210+
211+
4. MySQLDataSource.start
212+
5. MongoDBDataSource.start
213+
6. RestServer.start
214+
215+
7. MySQLDataSource.postStart
216+
8. MongoDBDataSource.postStart
217+
9. RestServer.postStart
218+
219+
## Add custom life cycle observers by convention
220+
221+
Each application can have custom life cycle observers to be dropped into
222+
`src/observers` folder as classes implementing `LifeCycleObserver`.
223+
224+
During application.boot(), such artifacts are discovered, loaded, and bound to
225+
the application context as life cycle observers. This is achieved by a built-in
226+
`LifeCycleObserverBooter` extension.
227+
228+
## CLI command
229+
230+
To make it easy for application developers to add custom life cycle observers,
231+
we introduce `lb4 observer` command as part the CLI.
232+
233+
To add a life cycle observer:
234+
235+
1. cd <my-loopback4-project>
236+
2. lb4 observer
237+
238+
```
239+
? Observer name: Hello
240+
create src/observers/my.hello-observer.ts
241+
update src/observers/index.ts
242+
243+
Observer Hello was created in src/observers/
244+
```
245+
246+
The generated class looks like:
247+
248+
```ts
249+
import {bind} from '@loopback/context';
250+
import {
251+
/* inject, Application, */
252+
CoreBindings,
253+
LifeCycleObserver,
254+
} from '@loopback/core';
255+
256+
/**
257+
* This class will be bound to the application as a `LifeCycleObserver` during
258+
* `boot`
259+
*/
260+
@bind({tags: {[CoreBindings.LIFE_CYCLE_OBSERVER_GROUP]: ''}})
261+
export class HelloObserver implements LifeCycleObserver {
262+
/*
263+
constructor(
264+
@inject(CoreBindings.APPLICATION_INSTANCE) private app: Application,
265+
) {}
266+
*/
267+
268+
/**
269+
* This method will be invoked when the application starts
270+
*/
271+
async start(): Promise<void> {
272+
// Add your logic for start
273+
}
274+
275+
/**
276+
* This method will be invoked when the application stops
277+
*/
278+
async stop(): Promise<void> {
279+
// Add your logic for start
280+
}
281+
}
282+
```

docs/site/sidebars/lb4_sidebar.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ children:
145145
url: DataSources.html
146146
output: 'web, pdf'
147147

148+
- title: 'Life cycle events and observers'
149+
url: Life-cycle.html
150+
output: 'web, pdf'
151+
148152
- title: 'Routes'
149153
url: Routes.html
150154
output: 'web, pdf'

packages/core/docs.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
"src/component.ts",
66
"src/index.ts",
77
"src/keys.ts",
8-
"src/server.ts"
8+
"src/server.ts",
9+
"src/lifecycle.ts"
910
],
1011
"codeSectionDepth": 4
1112
}

packages/core/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@
2020
"copyright.owner": "IBM Corp.",
2121
"license": "MIT",
2222
"dependencies": {
23-
"@loopback/context": "^1.9.0"
23+
"@loopback/context": "^1.9.0",
24+
"debug": "^4.1.0"
2425
},
2526
"devDependencies": {
2627
"@loopback/build": "^1.4.1",
2728
"@loopback/testlab": "^1.2.2",
2829
"@loopback/tslint-config": "^2.0.4",
30+
"@types/debug": "^4.1.2",
2931
"@types/node": "^10.11.2"
3032
},
3133
"files": [

0 commit comments

Comments
 (0)