@nestjs/core
Version:
Nest - modern, fast, powerful node.js web framework (@core)
355 lines (354 loc) • 13.7 kB
JavaScript
import { Logger, ShutdownSignal, } from '@nestjs/common';
import { MESSAGES } from './constants.js';
import { UnknownModuleException } from './errors/exceptions/index.js';
import { createContextId } from './helpers/context-id-factory.js';
import { callAppShutdownHook, callBeforeAppShutdownHook, callModuleBootstrapHook, callModuleDestroyHook, callModuleInitHook, } from './hooks/index.js';
import { AbstractInstanceResolver } from './injector/abstract-instance-resolver.js';
import { Injector } from './injector/injector.js';
import { InstanceLinksHost } from './injector/instance-links-host.js';
import { isEmptyArray, } from '@nestjs/common/internal';
/**
* @publicApi
*/
export class NestApplicationContext extends AbstractInstanceResolver {
container;
appOptions;
contextModule;
scope;
isInitialized = false;
injector;
logger = new Logger(NestApplicationContext.name, {
timestamp: true,
});
shouldFlushLogsOnOverride = false;
shutdownCleanupRefs = new Map();
moduleCompiler;
shutdownPromise;
receivedSignal = false;
_instanceLinksHost;
_moduleRefsForHooksByDistance;
initializationPromise;
get instanceLinksHost() {
if (!this._instanceLinksHost) {
this._instanceLinksHost = new InstanceLinksHost(this.container);
}
return this._instanceLinksHost;
}
constructor(container, appOptions = {}, contextModule = null, scope = new Array()) {
super();
this.container = container;
this.appOptions = appOptions;
this.contextModule = contextModule;
this.scope = scope;
this.injector = new Injector();
this.moduleCompiler = container.getModuleCompiler();
if (this.appOptions.preview) {
this.printInPreviewModeWarning();
}
}
selectContextModule() {
const modules = this.container.getModules().values();
this.contextModule = modules.next().value;
}
/**
* Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.
* @returns {INestApplicationContext}
*/
select(moduleType, selectOptions) {
const modulesContainer = this.container.getModules();
const contextModuleCtor = this.contextModule.metatype;
const scope = this.scope.concat(contextModuleCtor);
const moduleTokenFactory = this.container.getModuleTokenFactory();
const { type, dynamicMetadata } = this.moduleCompiler.extractMetadata(moduleType);
const token = dynamicMetadata
? moduleTokenFactory.createForDynamic(type, dynamicMetadata, moduleType)
: moduleTokenFactory.createForStatic(type, moduleType);
const selectedModule = modulesContainer.get(token);
if (!selectedModule) {
throw new UnknownModuleException(type.name);
}
const options = typeof selectOptions?.abortOnError !== 'undefined'
? {
...this.appOptions,
...selectOptions,
}
: this.appOptions;
return new NestApplicationContext(this.container, options, selectedModule, scope);
}
/**
* Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
* @returns {TResult | Array<TResult>}
*/
get(typeOrToken, options = { strict: false }) {
return !(options && options.strict)
? this.find(typeOrToken, options)
: this.find(typeOrToken, {
moduleId: this.contextModule?.id,
each: options.each,
});
}
/**
* Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
* @returns {Promise<TResult | Array<TResult>>}
*/
resolve(typeOrToken, contextId = createContextId(), options = { strict: false }) {
return this.resolvePerContext(typeOrToken, this.contextModule, contextId, options);
}
/**
* Registers the request/context object for a given context ID (DI container sub-tree).
* @returns {void}
*/
registerRequestByContextId(request, contextId) {
this.container.registerRequestProvider(request, contextId);
}
/**
* Initializes the Nest application.
* Calls the Nest lifecycle events.
*
* @returns {Promise<this>} The NestApplicationContext instance as Promise
*/
async init() {
if (this.isInitialized) {
return this;
}
this.initializationPromise = this.callInitHook().then(() => this.callBootstrapHook());
try {
await this.initializationPromise;
}
catch (err) {
// A successful initialization leaves the buffered logs to "useLogger()".
// A failed one flushes them: "createApplicationContext()" never returns
// the context to call it on.
this.flushLogsIfAutoFlushEnabled();
throw err;
}
this.isInitialized = true;
return this;
}
/**
* Terminates the application
* @returns {Promise<void>}
*/
async close(signal) {
await this.shutdown(signal);
}
/**
* Runs the shutdown sequence, at most once per cycle. Callers that arrive
* while a shutdown is already in flight - a process signal delivered during
* an explicit `close()`, or the other way round - await the very same
* promise instead of starting a second, concurrent teardown.
*
* @param {string} [signal] The system signal that triggered the shutdown
* @returns {Promise<void>}
*/
shutdown(signal) {
this.shutdownPromise ??= this.runShutdownSequence(signal).finally(() => {
// Let the context be shut down again once this cycle has settled,
// successfully or not.
this.shutdownPromise = undefined;
});
return this.shutdownPromise;
}
async runShutdownSequence(signal) {
// A shutdown that arrives during initialization waits for it to settle.
// A failed initialization must not stop the teardown: its error already
// went to the caller of `init()`, and the hooks below release what the
// partial startup acquired (connection pools, timers, sockets).
await this.initializationPromise?.catch(() => undefined);
await this.prepareClose();
await this.callDestroyHook();
await this.callBeforeShutdownHook(signal);
await this.dispose();
await this.callShutdownHook(signal);
this.unsubscribeFromProcessSignals();
}
/**
* Sets custom logger service.
* Flushes buffered logs if auto flush is on.
* @returns {void}
*/
useLogger(logger) {
Logger.overrideLogger(logger);
if (this.shouldFlushLogsOnOverride) {
this.flushLogs();
}
}
/**
* Prints buffered logs and detaches buffer.
* @returns {void}
*/
flushLogs() {
Logger.flush();
}
flushLogsIfAutoFlushEnabled() {
if (this.appOptions?.autoFlushLogs ?? true) {
this.flushLogs();
}
}
/**
* Define that it must flush logs right after defining a custom logger.
*/
flushLogsOnOverride() {
this.shouldFlushLogsOnOverride = true;
}
/**
* Enables the usage of shutdown hooks. Will call the
* `onApplicationShutdown` function of a provider if the
* process receives a shutdown signal.
*
* Repeated calls are idempotent per signal. Shutdown hooks can be
* re-enabled after the application context has been closed.
*
* @param {ShutdownSignal[]} [signals=[]] The system signals it should listen to
* @param {ShutdownHooksOptions} [options={}] Options for configuring shutdown hooks behavior
*
* @returns {this} The Nest application context instance
*/
enableShutdownHooks(signals = [], options = {}) {
if (!signals || isEmptyArray(signals)) {
signals = Object.values(ShutdownSignal);
}
signals = Array.from(new Set(signals.map((signal) => signal.toString().toUpperCase().trim()))).filter(signal => !this.shutdownCleanupRefs.has(signal));
this.listenToShutdownSignals(signals, options);
return this;
}
async prepareClose() {
// Nest application context has no server
// to signal, therefore just call a noop
return Promise.resolve();
}
async dispose() {
// Nest application context has no server
// to dispose, therefore just call a noop
return Promise.resolve();
}
/**
* Listens to shutdown signals by listening to
* process events
*
* @param {string[]} signals The system signals it should listen to
* @param {ShutdownHooksOptions} options Options for configuring shutdown hooks behavior
*/
listenToShutdownSignals(signals, options = {}) {
const cleanup = async (signal) => {
try {
if (this.receivedSignal) {
// Another signal's handler is already shutting down and will
// terminate the process.
return;
}
this.receivedSignal = true;
await this.shutdown(signal).finally(() => {
this.receivedSignal = false;
});
if (options.useProcessExit) {
// Use process.exit() to ensure the 'exit' event is properly triggered.
// This is required for async loggers (like Pino with transports)
// to flush their buffers before the process terminates.
process.exit(0);
}
else {
process.kill(process.pid, signal);
}
}
catch (err) {
Logger.error(MESSAGES.ERROR_DURING_SHUTDOWN, err?.stack, NestApplicationContext.name);
process.exit(1);
}
};
signals.forEach((signal) => {
this.shutdownCleanupRefs.set(signal, cleanup);
process.on(signal, cleanup);
});
}
/**
* Unsubscribes from shutdown signals (process events)
*/
unsubscribeFromProcessSignals() {
this.shutdownCleanupRefs.forEach((cleanup, signal) => {
process.removeListener(signal, cleanup);
});
this.shutdownCleanupRefs.clear();
}
/**
* Calls the `onModuleInit` function on the registered
* modules and its children.
*/
async callInitHook() {
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
for (const module of modulesSortedByDistance) {
await callModuleInitHook(module);
}
}
/**
* Calls the `onModuleDestroy` function on the registered
* modules and its children.
*/
async callDestroyHook() {
const modulesSortedByDistance = [
...this.getModulesToTriggerHooksOn(),
].reverse();
for (const module of modulesSortedByDistance) {
await callModuleDestroyHook(module);
}
}
/**
* Calls the `onApplicationBootstrap` function on the registered
* modules and its children.
*/
async callBootstrapHook() {
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
for (const module of modulesSortedByDistance) {
await callModuleBootstrapHook(module);
}
}
/**
* Calls the `onApplicationShutdown` function on the registered
* modules and children.
*/
async callShutdownHook(signal) {
const modulesSortedByDistance = [
...this.getModulesToTriggerHooksOn(),
].reverse();
for (const module of modulesSortedByDistance) {
await callAppShutdownHook(module, signal);
}
}
/**
* Calls the `beforeApplicationShutdown` function on the registered
* modules and children.
*/
async callBeforeShutdownHook(signal) {
const modulesSortedByDistance = [
...this.getModulesToTriggerHooksOn(),
].reverse();
for (const module of modulesSortedByDistance) {
await callBeforeAppShutdownHook(module, signal);
}
}
assertNotInPreviewMode(methodName) {
if (this.appOptions.preview) {
const error = `Calling the "${methodName}" in the preview mode is not supported.`;
this.logger.error(error);
throw new Error(error);
}
}
getModulesToTriggerHooksOn() {
if (this._moduleRefsForHooksByDistance) {
return this._moduleRefsForHooksByDistance;
}
const modulesContainer = this.container.getModules();
const compareFn = (a, b) => b.distance - a.distance;
const modulesSortedByDistance = Array.from(modulesContainer.values()).sort(compareFn);
this._moduleRefsForHooksByDistance = this.appOptions?.preview
? modulesSortedByDistance.filter(moduleRef => moduleRef.initOnPreview)
: modulesSortedByDistance;
return this._moduleRefsForHooksByDistance;
}
printInPreviewModeWarning() {
this.logger.warn('------------------------------------------------');
this.logger.warn('Application is running in the PREVIEW mode!');
this.logger.warn('Providers/controllers will not be instantiated.');
this.logger.warn('------------------------------------------------');
}
}