1 | ;
|
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
|
6 | Object.defineProperty(exports, "__esModule", { value: true });
|
7 | exports.Application = void 0;
|
8 | const tslib_1 = require("tslib");
|
9 | const context_1 = require("@loopback/context");
|
10 | const assert_1 = tslib_1.__importDefault(require("assert"));
|
11 | const debug_1 = tslib_1.__importDefault(require("debug"));
|
12 | const events_1 = require("events");
|
13 | const component_1 = require("./component");
|
14 | const keys_1 = require("./keys");
|
15 | const lifecycle_1 = require("./lifecycle");
|
16 | const lifecycle_registry_1 = require("./lifecycle-registry");
|
17 | const service_1 = require("./service");
|
18 | const debug = (0, debug_1.default)('loopback:core:application');
|
19 | const debugShutdown = (0, debug_1.default)('loopback:core:application:shutdown');
|
20 | const 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 | */
|
26 | function 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 | */
|
44 | class 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 | }
|
543 | exports.Application = Application;
|
544 | /**
|
545 | * Normalize name or options to `BindingFromClassOptions`
|
546 | * @param nameOrOptions - Name or options for binding from class
|
547 | */
|
548 | function 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 |