UNPKG

21.9 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
2// Node module: @loopback/core
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6import {
7 Binding,
8 BindingFromClassOptions,
9 BindingScope,
10 Constructor,
11 Context,
12 createBindingFromClass,
13 DynamicValueProviderClass,
14 generateUniqueId,
15 Interceptor,
16 InterceptorBindingOptions,
17 JSONObject,
18 Provider,
19 registerInterceptor,
20 ValueOrPromise,
21} from '@loopback/context';
22import assert from 'assert';
23import debugFactory from 'debug';
24import {once} from 'events';
25import {Component, mountComponent} from './component';
26import {CoreBindings, CoreTags} from './keys';
27import {
28 asLifeCycleObserver,
29 isLifeCycleObserverClass,
30 LifeCycleObserver,
31} from './lifecycle';
32import {LifeCycleObserverRegistry} from './lifecycle-registry';
33import {Server} from './server';
34import {createServiceBinding, ServiceOptions} from './service';
35const debug = debugFactory('loopback:core:application');
36const debugShutdown = debugFactory('loopback:core:application:shutdown');
37const debugWarning = debugFactory('loopback:core:application:warning');
38
39/**
40 * A helper function to build constructor args for `Context`
41 * @param configOrParent - Application config or parent context
42 * @param parent - Parent context if the first arg is application config
43 */
44function buildConstructorArgs(
45 configOrParent?: ApplicationConfig | Context,
46 parent?: Context,
47) {
48 let name: string | undefined;
49 let parentCtx: Context | undefined;
50
51 if (configOrParent instanceof Context) {
52 parentCtx = configOrParent;
53 name = undefined;
54 } else {
55 parentCtx = parent;
56 name = configOrParent?.name;
57 }
58 return [parentCtx, name];
59}
60
61/**
62 * Application is the container for various types of artifacts, such as
63 * components, servers, controllers, repositories, datasources, connectors,
64 * and models.
65 */
66export class Application extends Context implements LifeCycleObserver {
67 public readonly options: ApplicationConfig;
68
69 /**
70 * A flag to indicate that the application is being shut down
71 */
72 private _isShuttingDown = false;
73 private _shutdownOptions: ShutdownOptions;
74 private _signalListener: (signal: string) => Promise<void>;
75
76 private _initialized = false;
77
78 /**
79 * State of the application
80 */
81 private _state = 'created';
82
83 /**
84 * Get the state of the application. The initial state is `created` and it can
85 * transition as follows by `start` and `stop`:
86 *
87 * 1. start
88 * - !started -> starting -> started
89 * - started -> started (no-op)
90 * 2. stop
91 * - (started | initialized) -> stopping -> stopped
92 * - ! (started || initialized) -> stopped (no-op)
93 *
94 * Two types of states are expected:
95 * - stable, such as `started` and `stopped`
96 * - in process, such as `booting` and `starting`
97 *
98 * Operations such as `start` and `stop` can only be called at a stable state.
99 * The logic should immediately set the state to a new one indicating work in
100 * process, such as `starting` and `stopping`.
101 */
102 public get state() {
103 return this._state;
104 }
105
106 /**
107 * Create an application with the given parent context
108 * @param parent - Parent context
109 */
110 constructor(parent: Context);
111 /**
112 * Create an application with the given configuration and parent context
113 * @param config - Application configuration
114 * @param parent - Parent context
115 */
116 constructor(config?: ApplicationConfig, parent?: Context);
117
118 constructor(configOrParent?: ApplicationConfig | Context, parent?: Context) {
119 // super() has to be first statement for a constructor
120 super(...buildConstructorArgs(configOrParent, parent));
121 this.scope = BindingScope.APPLICATION;
122
123 this.options =
124 configOrParent instanceof Context ? {} : configOrParent ?? {};
125
126 // Configure debug
127 this._debug = debug;
128
129 // Bind the life cycle observer registry
130 this.bind(CoreBindings.LIFE_CYCLE_OBSERVER_REGISTRY)
131 .toClass(LifeCycleObserverRegistry)
132 .inScope(BindingScope.SINGLETON);
133 // Bind to self to allow injection of application context in other modules.
134 this.bind(CoreBindings.APPLICATION_INSTANCE).to(this);
135 // Make options available to other modules as well.
136 this.bind(CoreBindings.APPLICATION_CONFIG).to(this.options);
137
138 // Also configure the application instance to allow `@config`
139 this.configure(CoreBindings.APPLICATION_INSTANCE).toAlias(
140 CoreBindings.APPLICATION_CONFIG,
141 );
142
143 this._shutdownOptions = {signals: ['SIGTERM'], ...this.options.shutdown};
144 }
145
146 /**
147 * Register a controller class with this application.
148 *
149 * @param controllerCtor - The controller class
150 * (constructor function).
151 * @param name - Optional controller name, default to the class name
152 * @returns The newly created binding, you can use the reference to
153 * further modify the binding, e.g. lock the value to prevent further
154 * modifications.
155 *
156 * @example
157 * ```ts
158 * class MyController {
159 * }
160 * app.controller(MyController).lock();
161 * ```
162 */
163 controller<T>(
164 controllerCtor: ControllerClass<T>,
165 nameOrOptions?: string | BindingFromClassOptions,
166 ): Binding<T> {
167 this.debug('Adding controller %s', nameOrOptions ?? controllerCtor.name);
168 const binding = createBindingFromClass(controllerCtor, {
169 namespace: CoreBindings.CONTROLLERS,
170 type: CoreTags.CONTROLLER,
171 defaultScope: BindingScope.TRANSIENT,
172 ...toOptions(nameOrOptions),
173 });
174 this.add(binding);
175 return binding;
176 }
177
178 /**
179 * Bind a Server constructor to the Application's master context.
180 * Each server constructor added in this way must provide a unique prefix
181 * to prevent binding overlap.
182 *
183 * @example
184 * ```ts
185 * app.server(RestServer);
186 * // This server constructor will be bound under "servers.RestServer".
187 * app.server(RestServer, "v1API");
188 * // This server instance will be bound under "servers.v1API".
189 * ```
190 *
191 * @param server - The server constructor.
192 * @param nameOrOptions - Optional override for name or options.
193 * @returns Binding for the server class
194 *
195 */
196 public server<T extends Server>(
197 ctor: Constructor<T>,
198 nameOrOptions?: string | BindingFromClassOptions,
199 ): Binding<T> {
200 this.debug('Adding server %s', nameOrOptions ?? ctor.name);
201 const binding = createBindingFromClass(ctor, {
202 namespace: CoreBindings.SERVERS,
203 type: CoreTags.SERVER,
204 defaultScope: BindingScope.SINGLETON,
205 ...toOptions(nameOrOptions),
206 }).apply(asLifeCycleObserver);
207 this.add(binding);
208 return binding;
209 }
210
211 /**
212 * Bind an array of Server constructors to the Application's master
213 * context.
214 * Each server added in this way will automatically be named based on the
215 * class constructor name with the "servers." prefix.
216 *
217 * @remarks
218 * If you wish to control the binding keys for particular server instances,
219 * use the app.server function instead.
220 * ```ts
221 * app.servers([
222 * RestServer,
223 * GRPCServer,
224 * ]);
225 * // Creates a binding for "servers.RestServer" and a binding for
226 * // "servers.GRPCServer";
227 * ```
228 *
229 * @param ctors - An array of Server constructors.
230 * @returns An array of bindings for the registered server classes
231 *
232 */
233 public servers<T extends Server>(ctors: Constructor<T>[]): Binding[] {
234 return ctors.map(ctor => this.server(ctor));
235 }
236
237 /**
238 * Retrieve the singleton instance for a bound server.
239 *
240 * @typeParam T - Server type
241 * @param ctor - The constructor that was used to make the
242 * binding.
243 * @returns A Promise of server instance
244 *
245 */
246 public async getServer<T extends Server>(
247 target: Constructor<T> | string,
248 ): Promise<T> {
249 let key: string;
250 // instanceof check not reliable for string.
251 if (typeof target === 'string') {
252 key = `${CoreBindings.SERVERS}.${target}`;
253 } else {
254 const ctor = target as Constructor<T>;
255 key = `${CoreBindings.SERVERS}.${ctor.name}`;
256 }
257 return this.get<T>(key);
258 }
259
260 /**
261 * Assert there is no other operation is in progress, i.e., the state is not
262 * `*ing`, such as `starting` or `stopping`.
263 *
264 * @param op - The operation name, such as 'boot', 'start', or 'stop'
265 */
266 protected assertNotInProcess(op: string) {
267 assert(
268 !this._state.endsWith('ing'),
269 `Cannot ${op} the application as it is ${this._state}.`,
270 );
271 }
272
273 /**
274 * Assert current state of the application to be one of the expected values
275 * @param op - The operation name, such as 'boot', 'start', or 'stop'
276 * @param states - Valid states
277 */
278 protected assertInStates(op: string, ...states: string[]) {
279 assert(
280 states.includes(this._state),
281 `Cannot ${op} the application as it is ${this._state}. Valid states are ${states}.`,
282 );
283 }
284
285 /**
286 * Transition the application to a new state and emit an event
287 * @param state - The new state
288 */
289 protected setState(state: string) {
290 const oldState = this._state;
291 this._state = state;
292 if (oldState !== state) {
293 this.emit('stateChanged', {from: oldState, to: this._state});
294 this.emit(state);
295 }
296 }
297
298 protected async awaitState(state: string) {
299 await once(this, state);
300 }
301
302 /**
303 * Initialize the application, and all of its registered observers. The
304 * application state is checked to ensure the integrity of `initialize`.
305 *
306 * If the application is already initialized, no operation is performed.
307 *
308 * This method is automatically invoked by `start()` if the application is not
309 * initialized.
310 */
311 public async init(): Promise<void> {
312 if (this._initialized) return;
313 if (this._state === 'initializing') return this.awaitState('initialized');
314 this.assertNotInProcess('initialize');
315 this.setState('initializing');
316
317 const registry = await this.getLifeCycleObserverRegistry();
318 await registry.init();
319 this._initialized = true;
320 this.setState('initialized');
321 }
322
323 /**
324 * Register a function to be called when the application initializes.
325 *
326 * This is a shortcut for adding a binding for a LifeCycleObserver
327 * implementing a `init()` method.
328 *
329 * @param fn The function to invoke, it can be synchronous (returning `void`)
330 * or asynchronous (returning `Promise<void>`).
331 * @returns The LifeCycleObserver binding created.
332 */
333 public onInit(fn: () => ValueOrPromise<void>): Binding<LifeCycleObserver> {
334 const key = [
335 CoreBindings.LIFE_CYCLE_OBSERVERS,
336 fn.name || '<onInit>',
337 generateUniqueId(),
338 ].join('.');
339
340 return this.bind<LifeCycleObserver>(key)
341 .to({init: fn})
342 .apply(asLifeCycleObserver);
343 }
344
345 /**
346 * Start the application, and all of its registered observers. The application
347 * state is checked to ensure the integrity of `start`.
348 *
349 * If the application is not initialized, it calls first `init()` to
350 * initialize the application. This only happens if `start()` is called for
351 * the first time.
352 *
353 * If the application is already started, no operation is performed.
354 */
355 public async start(): Promise<void> {
356 if (!this._initialized) await this.init();
357 if (this._state === 'starting') return this.awaitState('started');
358 this.assertNotInProcess('start');
359 // No-op if it's started
360 if (this._state === 'started') return;
361 this.setState('starting');
362 this.setupShutdown();
363
364 const registry = await this.getLifeCycleObserverRegistry();
365 await registry.start();
366 this.setState('started');
367 }
368
369 /**
370 * Register a function to be called when the application starts.
371 *
372 * This is a shortcut for adding a binding for a LifeCycleObserver
373 * implementing a `start()` method.
374 *
375 * @param fn The function to invoke, it can be synchronous (returning `void`)
376 * or asynchronous (returning `Promise<void>`).
377 * @returns The LifeCycleObserver binding created.
378 */
379 public onStart(fn: () => ValueOrPromise<void>): Binding<LifeCycleObserver> {
380 const key = [
381 CoreBindings.LIFE_CYCLE_OBSERVERS,
382 fn.name || '<onStart>',
383 generateUniqueId(),
384 ].join('.');
385
386 return this.bind<LifeCycleObserver>(key)
387 .to({start: fn})
388 .apply(asLifeCycleObserver);
389 }
390
391 /**
392 * Stop the application instance and all of its registered observers. The
393 * application state is checked to ensure the integrity of `stop`.
394 *
395 * If the application is already stopped or not started, no operation is
396 * performed.
397 */
398 public async stop(): Promise<void> {
399 if (this._state === 'stopping') return this.awaitState('stopped');
400 this.assertNotInProcess('stop');
401 // No-op if it's created or stopped
402 if (this._state !== 'started' && this._state !== 'initialized') return;
403 this.setState('stopping');
404 if (!this._isShuttingDown) {
405 // Explicit stop is called, let's remove signal listeners to avoid
406 // memory leak and max listener warning
407 this.removeSignalListener();
408 }
409 const registry = await this.getLifeCycleObserverRegistry();
410 await registry.stop();
411 this.setState('stopped');
412 }
413
414 /**
415 * Register a function to be called when the application starts.
416 *
417 * This is a shortcut for adding a binding for a LifeCycleObserver
418 * implementing a `start()` method.
419 *
420 * @param fn The function to invoke, it can be synchronous (returning `void`)
421 * or asynchronous (returning `Promise<void>`).
422 * @returns The LifeCycleObserver binding created.
423 */
424 public onStop(fn: () => ValueOrPromise<void>): Binding<LifeCycleObserver> {
425 const key = [
426 CoreBindings.LIFE_CYCLE_OBSERVERS,
427 fn.name || '<onStop>',
428 generateUniqueId(),
429 ].join('.');
430 return this.bind<LifeCycleObserver>(key)
431 .to({stop: fn})
432 .apply(asLifeCycleObserver);
433 }
434
435 private async getLifeCycleObserverRegistry() {
436 return this.get(CoreBindings.LIFE_CYCLE_OBSERVER_REGISTRY);
437 }
438
439 /**
440 * Add a component to this application and register extensions such as
441 * controllers, providers, and servers from the component.
442 *
443 * @param componentCtor - The component class to add.
444 * @param nameOrOptions - Optional component name or options, default to the
445 * class name
446 *
447 * @example
448 * ```ts
449 *
450 * export class ProductComponent {
451 * controllers = [ProductController];
452 * repositories = [ProductRepo, UserRepo];
453 * providers = {
454 * [AUTHENTICATION_STRATEGY]: AuthStrategy,
455 * [AUTHORIZATION_ROLE]: Role,
456 * };
457 * };
458 *
459 * app.component(ProductComponent);
460 * ```
461 */
462 public component<T extends Component = Component>(
463 componentCtor: Constructor<T>,
464 nameOrOptions?: string | BindingFromClassOptions,
465 ) {
466 this.debug('Adding component: %s', nameOrOptions ?? componentCtor.name);
467 const binding = createBindingFromClass(componentCtor, {
468 namespace: CoreBindings.COMPONENTS,
469 type: CoreTags.COMPONENT,
470 defaultScope: BindingScope.SINGLETON,
471 ...toOptions(nameOrOptions),
472 });
473 if (isLifeCycleObserverClass(componentCtor)) {
474 binding.apply(asLifeCycleObserver);
475 }
476 this.add(binding);
477 // Assuming components can be synchronously instantiated
478 const instance = this.getSync<Component>(binding.key);
479 mountComponent(this, instance);
480 return binding;
481 }
482
483 /**
484 * Set application metadata. `@loopback/boot` calls this method to populate
485 * the metadata from `package.json`.
486 *
487 * @param metadata - Application metadata
488 */
489 public setMetadata(metadata: ApplicationMetadata) {
490 this.bind(CoreBindings.APPLICATION_METADATA).to(metadata);
491 }
492
493 /**
494 * Register a life cycle observer class
495 * @param ctor - A class implements LifeCycleObserver
496 * @param nameOrOptions - Optional name or options for the life cycle observer
497 */
498 public lifeCycleObserver<T extends LifeCycleObserver>(
499 ctor: Constructor<T>,
500 nameOrOptions?: string | BindingFromClassOptions,
501 ): Binding<T> {
502 this.debug('Adding life cycle observer %s', nameOrOptions ?? ctor.name);
503 const binding = createBindingFromClass(ctor, {
504 namespace: CoreBindings.LIFE_CYCLE_OBSERVERS,
505 type: CoreTags.LIFE_CYCLE_OBSERVER,
506 defaultScope: BindingScope.SINGLETON,
507 ...toOptions(nameOrOptions),
508 }).apply(asLifeCycleObserver);
509 this.add(binding);
510 return binding;
511 }
512
513 /**
514 * Add a service to this application.
515 *
516 * @param cls - The service or provider class
517 *
518 * @example
519 *
520 * ```ts
521 * // Define a class to be bound via ctx.toClass()
522 * @injectable({scope: BindingScope.SINGLETON})
523 * export class LogService {
524 * log(msg: string) {
525 * console.log(msg);
526 * }
527 * }
528 *
529 * // Define a class to be bound via ctx.toProvider()
530 * import {v4 as uuidv4} from 'uuid';
531 * export class UuidProvider implements Provider<string> {
532 * value() {
533 * return uuidv4();
534 * }
535 * }
536 *
537 * // Register the local services
538 * app.service(LogService);
539 * app.service(UuidProvider, 'uuid');
540 *
541 * export class MyController {
542 * constructor(
543 * @inject('services.uuid') private uuid: string,
544 * @inject('services.LogService') private log: LogService,
545 * ) {
546 * }
547 *
548 * greet(name: string) {
549 * this.log(`Greet request ${this.uuid} received: ${name}`);
550 * return `${this.uuid}: ${name}`;
551 * }
552 * }
553 * ```
554 */
555 public service<S>(
556 cls: ServiceOrProviderClass<S>,
557 nameOrOptions?: string | ServiceOptions,
558 ): Binding<S> {
559 const options = toOptions(nameOrOptions);
560 const binding = createServiceBinding(cls, options);
561 this.add(binding);
562 return binding;
563 }
564
565 /**
566 * Register an interceptor
567 * @param interceptor - An interceptor function or provider class
568 * @param nameOrOptions - Binding name or options
569 */
570 public interceptor(
571 interceptor: Interceptor | Constructor<Provider<Interceptor>>,
572 nameOrOptions?: string | InterceptorBindingOptions,
573 ) {
574 const options = toOptions(nameOrOptions);
575 return registerInterceptor(this, interceptor, options);
576 }
577
578 /**
579 * Set up signals that are captured to shutdown the application
580 */
581 protected setupShutdown() {
582 if (this._signalListener != null) {
583 this.registerSignalListener();
584 return this._signalListener;
585 }
586 const gracePeriod = this._shutdownOptions.gracePeriod;
587 this._signalListener = async (signal: string) => {
588 const kill = () => {
589 this.removeSignalListener();
590 process.kill(process.pid, signal);
591 };
592 debugShutdown(
593 '[%s] Signal %s received for process %d',
594 this.name,
595 signal,
596 process.pid,
597 );
598 if (!this._isShuttingDown) {
599 this._isShuttingDown = true;
600 let timer;
601 if (typeof gracePeriod === 'number' && !isNaN(gracePeriod)) {
602 timer = setTimeout(kill, gracePeriod);
603 }
604 try {
605 await this.stop();
606 } finally {
607 if (timer != null) clearTimeout(timer);
608 kill();
609 }
610 }
611 };
612 this.registerSignalListener();
613 return this._signalListener;
614 }
615
616 private registerSignalListener() {
617 const {signals = []} = this._shutdownOptions;
618 debugShutdown(
619 '[%s] Registering signal listeners on the process %d',
620 this.name,
621 process.pid,
622 signals,
623 );
624 signals.forEach(sig => {
625 if (process.getMaxListeners() <= process.listenerCount(sig)) {
626 if (debugWarning.enabled) {
627 debugWarning(
628 '[%s] %d %s listeners are added to process %d',
629 this.name,
630 process.listenerCount(sig),
631 sig,
632 process.pid,
633 new Error('MaxListenersExceededWarning'),
634 );
635 }
636 }
637 // eslint-disable-next-line @typescript-eslint/no-misused-promises
638 process.on(sig, this._signalListener);
639 });
640 }
641
642 private removeSignalListener() {
643 if (this._signalListener == null) return;
644 const {signals = []} = this._shutdownOptions;
645 debugShutdown(
646 '[%s] Removing signal listeners on the process %d',
647 this.name,
648 process.pid,
649 signals,
650 );
651 signals.forEach(sig =>
652 // eslint-disable-next-line @typescript-eslint/no-misused-promises
653 process.removeListener(sig, this._signalListener),
654 );
655 }
656}
657
658/**
659 * Normalize name or options to `BindingFromClassOptions`
660 * @param nameOrOptions - Name or options for binding from class
661 */
662function toOptions(nameOrOptions?: string | BindingFromClassOptions) {
663 if (typeof nameOrOptions === 'string') {
664 return {name: nameOrOptions};
665 }
666 return nameOrOptions ?? {};
667}
668
669/**
670 * Options to set up application shutdown
671 */
672export type ShutdownOptions = {
673 /**
674 * An array of signals to be trapped for graceful shutdown
675 */
676 signals?: NodeJS.Signals[];
677 /**
678 * Period in milliseconds to wait for the grace shutdown to finish before
679 * exiting the process
680 */
681 gracePeriod?: number;
682};
683
684/**
685 * Configuration for application
686 */
687export interface ApplicationConfig {
688 /**
689 * Name of the application context
690 */
691 name?: string;
692 /**
693 * Configuration for signals that shut down the application
694 */
695 shutdown?: ShutdownOptions;
696
697 /**
698 * Other properties
699 */
700 // eslint-disable-next-line @typescript-eslint/no-explicit-any
701 [prop: string]: any;
702}
703
704// eslint-disable-next-line @typescript-eslint/no-explicit-any
705export type ControllerClass<T = any> = Constructor<T>;
706
707// eslint-disable-next-line @typescript-eslint/no-explicit-any
708export type ServiceOrProviderClass<T = any> =
709 | Constructor<T | Provider<T>>
710 | DynamicValueProviderClass<T>;
711
712/**
713 * Type description for `package.json`
714 */
715export interface ApplicationMetadata extends JSONObject {
716 name: string;
717 version: string;
718 description: string;
719}