UNPKG

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