mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
513 lines
16 KiB
TypeScript
513 lines
16 KiB
TypeScript
import {
|
|
INestApplicationContext,
|
|
Logger,
|
|
LoggerService,
|
|
LogLevel,
|
|
ShutdownSignal,
|
|
} from '@nestjs/common';
|
|
import {
|
|
Abstract,
|
|
DynamicModule,
|
|
GetOrResolveOptions,
|
|
SelectOptions,
|
|
ShutdownHooksOptions,
|
|
Type,
|
|
} from '@nestjs/common/interfaces';
|
|
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
|
import { isEmpty } from '@nestjs/common/utils/shared.utils';
|
|
import { iterate } from 'iterare';
|
|
import { MESSAGES } from './constants';
|
|
import { UnknownModuleException } from './errors/exceptions';
|
|
import { createContextId } from './helpers/context-id-factory';
|
|
import {
|
|
callAppShutdownHook,
|
|
callBeforeAppShutdownHook,
|
|
callModuleBootstrapHook,
|
|
callModuleDestroyHook,
|
|
callModuleInitHook,
|
|
} from './hooks';
|
|
import { AbstractInstanceResolver } from './injector/abstract-instance-resolver';
|
|
import { ModuleCompiler } from './injector/compiler';
|
|
import { NestContainer } from './injector/container';
|
|
import { Injector } from './injector/injector';
|
|
import { InstanceLinksHost } from './injector/instance-links-host';
|
|
import { ContextId } from './injector/instance-wrapper';
|
|
import { Module } from './injector/module';
|
|
|
|
/**
|
|
* @publicApi
|
|
*/
|
|
export class NestApplicationContext<
|
|
TOptions extends
|
|
NestApplicationContextOptions = NestApplicationContextOptions,
|
|
>
|
|
extends AbstractInstanceResolver
|
|
implements INestApplicationContext
|
|
{
|
|
protected isInitialized = false;
|
|
protected injector: Injector;
|
|
protected readonly logger = new Logger(NestApplicationContext.name, {
|
|
timestamp: true,
|
|
});
|
|
|
|
private shouldFlushLogsOnOverride = false;
|
|
private readonly activeShutdownSignals = new Array<string>();
|
|
private readonly moduleCompiler: ModuleCompiler;
|
|
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
|
|
private _instanceLinksHost: InstanceLinksHost;
|
|
private _moduleRefsForHooksByDistance?: Array<Module>;
|
|
private initializationPromise?: Promise<void>;
|
|
|
|
protected get instanceLinksHost() {
|
|
if (!this._instanceLinksHost) {
|
|
this._instanceLinksHost = new InstanceLinksHost(this.container);
|
|
}
|
|
return this._instanceLinksHost;
|
|
}
|
|
|
|
constructor(
|
|
protected readonly container: NestContainer,
|
|
protected readonly appOptions: TOptions = {} as TOptions,
|
|
private contextModule: Module | null = null,
|
|
private readonly scope = new Array<Type<any>>(),
|
|
) {
|
|
super();
|
|
this.injector = new Injector();
|
|
this.moduleCompiler = container.getModuleCompiler();
|
|
|
|
if (this.appOptions.preview) {
|
|
this.printInPreviewModeWarning();
|
|
}
|
|
}
|
|
|
|
public 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}
|
|
*/
|
|
public select<T>(
|
|
moduleType: Type<T> | DynamicModule,
|
|
selectOptions?: SelectOptions,
|
|
): INestApplicationContext {
|
|
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 as DynamicModule,
|
|
)
|
|
: moduleTokenFactory.createForStatic(type, moduleType as Type);
|
|
|
|
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 of either injectable or controller, otherwise, throws exception.
|
|
* @returns {TResult}
|
|
*/
|
|
public get<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
): TResult;
|
|
/**
|
|
* Retrieves an instance of either injectable or controller, otherwise, throws exception.
|
|
* @returns {TResult}
|
|
*/
|
|
public get<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
options: {
|
|
strict?: boolean;
|
|
each?: undefined | false;
|
|
},
|
|
): TResult;
|
|
/**
|
|
* Retrieves a list of instances of either injectables or controllers, otherwise, throws exception.
|
|
* @returns {Array<TResult>}
|
|
*/
|
|
public get<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
options: {
|
|
strict?: boolean;
|
|
each: true;
|
|
},
|
|
): Array<TResult>;
|
|
/**
|
|
* Retrieves an instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
|
|
* @returns {TResult | Array<TResult>}
|
|
*/
|
|
public get<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Abstract<TInput> | string | symbol,
|
|
options: GetOrResolveOptions = { strict: false },
|
|
): TResult | Array<TResult> {
|
|
return !(options && options.strict)
|
|
? this.find<TInput, TResult>(typeOrToken, options)
|
|
: this.find<TInput, TResult>(typeOrToken, {
|
|
moduleId: this.contextModule?.id,
|
|
each: options.each,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
|
|
* @returns {Array<TResult>}
|
|
*/
|
|
public resolve<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
): Promise<TResult>;
|
|
/**
|
|
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
|
|
* @returns {Array<TResult>}
|
|
*/
|
|
public resolve<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
contextId?: {
|
|
id: number;
|
|
},
|
|
): Promise<TResult>;
|
|
/**
|
|
* Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.
|
|
* @returns {Array<TResult>}
|
|
*/
|
|
public resolve<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
contextId?: {
|
|
id: number;
|
|
},
|
|
options?: {
|
|
strict?: boolean;
|
|
each?: undefined | false;
|
|
},
|
|
): Promise<TResult>;
|
|
/**
|
|
* Resolves transient or request-scoped instances of either injectables or controllers, otherwise, throws exception.
|
|
* @returns {Array<TResult>}
|
|
*/
|
|
public resolve<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Function | string | symbol,
|
|
contextId?: {
|
|
id: number;
|
|
},
|
|
options?: {
|
|
strict?: boolean;
|
|
each: true;
|
|
},
|
|
): Promise<Array<TResult>>;
|
|
/**
|
|
* Resolves transient or request-scoped instance (or a list of instances) of either injectable or controller, otherwise, throws exception.
|
|
* @returns {Promise<TResult | Array<TResult>>}
|
|
*/
|
|
public resolve<TInput = any, TResult = TInput>(
|
|
typeOrToken: Type<TInput> | Abstract<TInput> | string | symbol,
|
|
contextId = createContextId(),
|
|
options: GetOrResolveOptions = { strict: false },
|
|
): Promise<TResult | Array<TResult>> {
|
|
return this.resolvePerContext<TInput, TResult>(
|
|
typeOrToken,
|
|
this.contextModule!,
|
|
contextId,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers the request/context object for a given context ID (DI container sub-tree).
|
|
* @returns {void}
|
|
*/
|
|
public registerRequestByContextId<T = any>(request: T, contextId: ContextId) {
|
|
this.container.registerRequestProvider(request, contextId);
|
|
}
|
|
|
|
/**
|
|
* Initializes the Nest application.
|
|
* Calls the Nest lifecycle events.
|
|
*
|
|
* @returns {Promise<this>} The NestApplicationContext instance as Promise
|
|
*/
|
|
public async init(): Promise<this> {
|
|
if (this.isInitialized) {
|
|
return this;
|
|
}
|
|
/* eslint-disable-next-line no-async-promise-executor */
|
|
this.initializationPromise = new Promise(async (resolve, reject) => {
|
|
try {
|
|
await this.callInitHook();
|
|
await this.callBootstrapHook();
|
|
resolve();
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
await this.initializationPromise;
|
|
|
|
this.isInitialized = true;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Terminates the application
|
|
* @returns {Promise<void>}
|
|
*/
|
|
public async close(signal?: string): Promise<void> {
|
|
await this.initializationPromise;
|
|
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}
|
|
*/
|
|
public useLogger(logger: LoggerService | LogLevel[] | false) {
|
|
Logger.overrideLogger(logger);
|
|
|
|
if (this.shouldFlushLogsOnOverride) {
|
|
this.flushLogs();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prints buffered logs and detaches buffer.
|
|
* @returns {void}
|
|
*/
|
|
public flushLogs() {
|
|
Logger.flush();
|
|
}
|
|
|
|
/**
|
|
* Define that it must flush logs right after defining a custom logger.
|
|
*/
|
|
public 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.
|
|
*
|
|
* @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
|
|
*/
|
|
public enableShutdownHooks(
|
|
signals: (ShutdownSignal | string)[] = [],
|
|
options: ShutdownHooksOptions = {},
|
|
): this {
|
|
if (isEmpty(signals)) {
|
|
signals = Object.keys(ShutdownSignal).map(
|
|
(key: string) => ShutdownSignal[key],
|
|
);
|
|
} else {
|
|
// given signals array should be unique because
|
|
// process shouldn't listen to the same signal more than once.
|
|
signals = Array.from(new Set(signals));
|
|
}
|
|
|
|
signals = iterate(signals)
|
|
.map((signal: string) => signal.toString().toUpperCase().trim())
|
|
// filter out the signals which is already listening to
|
|
.filter(signal => !this.activeShutdownSignals.includes(signal))
|
|
.toArray();
|
|
|
|
this.listenToShutdownSignals(signals, options);
|
|
return this;
|
|
}
|
|
|
|
protected async dispose(): Promise<void> {
|
|
// 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
|
|
*/
|
|
protected listenToShutdownSignals(
|
|
signals: string[],
|
|
options: ShutdownHooksOptions = {},
|
|
) {
|
|
let receivedSignal = false;
|
|
const cleanup = async (signal: string) => {
|
|
try {
|
|
if (receivedSignal) {
|
|
// If we receive another signal while we're waiting
|
|
// for the server to stop, just ignore it.
|
|
return;
|
|
}
|
|
receivedSignal = true;
|
|
await this.initializationPromise;
|
|
await this.callDestroyHook();
|
|
await this.callBeforeShutdownHook(signal);
|
|
await this.dispose();
|
|
await this.callShutdownHook(signal);
|
|
signals.forEach(sig => process.removeListener(sig, cleanup));
|
|
|
|
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 as Error)?.stack,
|
|
NestApplicationContext.name,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
this.shutdownCleanupRef = cleanup as (...args: unknown[]) => unknown;
|
|
|
|
signals.forEach((signal: string) => {
|
|
this.activeShutdownSignals.push(signal);
|
|
process.on(signal as any, cleanup);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Unsubscribes from shutdown signals (process events)
|
|
*/
|
|
protected unsubscribeFromProcessSignals() {
|
|
if (!this.shutdownCleanupRef) {
|
|
return;
|
|
}
|
|
this.activeShutdownSignals.forEach(signal => {
|
|
process.removeListener(signal, this.shutdownCleanupRef!);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Calls the `onModuleInit` function on the registered
|
|
* modules and its children.
|
|
*/
|
|
protected async callInitHook(): Promise<void> {
|
|
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
|
|
for (const module of modulesSortedByDistance) {
|
|
await callModuleInitHook(module);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the `onModuleDestroy` function on the registered
|
|
* modules and its children.
|
|
*/
|
|
protected async callDestroyHook(): Promise<void> {
|
|
const modulesSortedByDistance = [
|
|
...this.getModulesToTriggerHooksOn(),
|
|
].reverse();
|
|
|
|
for (const module of modulesSortedByDistance) {
|
|
await callModuleDestroyHook(module);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the `onApplicationBootstrap` function on the registered
|
|
* modules and its children.
|
|
*/
|
|
protected async callBootstrapHook(): Promise<void> {
|
|
const modulesSortedByDistance = this.getModulesToTriggerHooksOn();
|
|
for (const module of modulesSortedByDistance) {
|
|
await callModuleBootstrapHook(module);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the `onApplicationShutdown` function on the registered
|
|
* modules and children.
|
|
*/
|
|
protected async callShutdownHook(signal?: string): Promise<void> {
|
|
const modulesSortedByDistance = [
|
|
...this.getModulesToTriggerHooksOn(),
|
|
].reverse();
|
|
|
|
for (const module of modulesSortedByDistance) {
|
|
await callAppShutdownHook(module, signal);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls the `beforeApplicationShutdown` function on the registered
|
|
* modules and children.
|
|
*/
|
|
protected async callBeforeShutdownHook(signal?: string): Promise<void> {
|
|
const modulesSortedByDistance = [
|
|
...this.getModulesToTriggerHooksOn(),
|
|
].reverse();
|
|
|
|
for (const module of modulesSortedByDistance) {
|
|
await callBeforeAppShutdownHook(module, signal);
|
|
}
|
|
}
|
|
|
|
protected assertNotInPreviewMode(methodName: string) {
|
|
if (this.appOptions.preview) {
|
|
const error = `Calling the "${methodName}" in the preview mode is not supported.`;
|
|
this.logger.error(error);
|
|
throw new Error(error);
|
|
}
|
|
}
|
|
|
|
private getModulesToTriggerHooksOn(): Module[] {
|
|
if (this._moduleRefsForHooksByDistance) {
|
|
return this._moduleRefsForHooksByDistance;
|
|
}
|
|
const modulesContainer = this.container.getModules();
|
|
const compareFn = (a: Module, b: Module) => 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;
|
|
}
|
|
|
|
private 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('------------------------------------------------');
|
|
}
|
|
}
|