Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): introduce different module opaque key factories #13336

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -44,4 +44,13 @@ export class NestApplicationContextOptions {
* @default false
*/
snapshot?: boolean;

/**
* Determines what algorithm use to generate module ids.
* When set to `deep-hash`, the module id is generated based on the serialized module definition.
* When set to `reference`, each module obtains a unique id based on its reference.
*
* @default 'reference'
*/
moduleIdGeneratorAlgorithm?: 'deep-hash' | 'reference';
}
52 changes: 37 additions & 15 deletions packages/core/injector/compiler.ts
Expand Up @@ -3,7 +3,7 @@ import {
ForwardReference,
Type,
} from '@nestjs/common/interfaces';
import { ModuleTokenFactory } from './module-token-factory';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';

export interface ModuleFactory {
type: Type<any>;
Expand All @@ -12,36 +12,58 @@ export interface ModuleFactory {
}

export class ModuleCompiler {
constructor(private readonly moduleTokenFactory = new ModuleTokenFactory()) {}
constructor(
private readonly _moduleOpaqueKeyFactory: ModuleOpaqueKeyFactory,
) {}

get moduleOpaqueKeyFactory(): ModuleOpaqueKeyFactory {
return this._moduleOpaqueKeyFactory;
}

public async compile(
metatype: Type<any> | DynamicModule | Promise<DynamicModule>,
moduleClsOrDynamic:
| Type
| DynamicModule
| ForwardReference
| Promise<DynamicModule>,
): Promise<ModuleFactory> {
const { type, dynamicMetadata } = this.extractMetadata(await metatype);
const token = this.moduleTokenFactory.create(type, dynamicMetadata);
moduleClsOrDynamic = await moduleClsOrDynamic;

const { type, dynamicMetadata } = this.extractMetadata(moduleClsOrDynamic);
const token = dynamicMetadata
? this._moduleOpaqueKeyFactory.createForDynamic(
type,
dynamicMetadata,
moduleClsOrDynamic as DynamicModule | ForwardReference,
)
: this._moduleOpaqueKeyFactory.createForStatic(
type,
moduleClsOrDynamic as Type,
);

return { type, dynamicMetadata, token };
}

public extractMetadata(
metatype: Type<any> | ForwardReference | DynamicModule,
moduleClsOrDynamic: Type | ForwardReference | DynamicModule,
): {
type: Type<any>;
type: Type;
dynamicMetadata?: Partial<DynamicModule> | undefined;
} {
if (!this.isDynamicModule(metatype)) {
if (!this.isDynamicModule(moduleClsOrDynamic)) {
return {
type: (metatype as ForwardReference)?.forwardRef
? (metatype as ForwardReference).forwardRef()
: metatype,
type: (moduleClsOrDynamic as ForwardReference)?.forwardRef
? (moduleClsOrDynamic as ForwardReference).forwardRef()
: moduleClsOrDynamic,
};
kamilmysliwiec marked this conversation as resolved.
Show resolved Hide resolved
}
const { module: type, ...dynamicMetadata } = metatype;
const { module: type, ...dynamicMetadata } = moduleClsOrDynamic;
return { type, dynamicMetadata };
kamilmysliwiec marked this conversation as resolved.
Show resolved Hide resolved
}

public isDynamicModule(
module: Type<any> | DynamicModule | ForwardReference,
): module is DynamicModule {
return !!(module as DynamicModule).module;
moduleClsOrDynamic: Type | DynamicModule | ForwardReference,
): moduleClsOrDynamic is DynamicModule {
return !!(moduleClsOrDynamic as DynamicModule).module;
}
}
31 changes: 24 additions & 7 deletions packages/core/injector/container.ts
Expand Up @@ -4,6 +4,7 @@ import {
GLOBAL_MODULE_METADATA,
} from '@nestjs/common/constants';
import { Injectable, Type } from '@nestjs/common/interfaces';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
import { ApplicationConfig } from '../application-config';
import { DiscoverableMetaHostCollection } from '../discovery/discoverable-meta-host-collection';
import {
Expand All @@ -19,28 +20,44 @@ import { ContextId } from './instance-wrapper';
import { InternalCoreModule } from './internal-core-module/internal-core-module';
import { InternalProvidersStorage } from './internal-providers-storage';
import { Module } from './module';
import { ModuleTokenFactory } from './module-token-factory';
import { ModulesContainer } from './modules-container';
import { ByReferenceModuleOpaqueKeyFactory } from './opaque-key-factory/by-reference-module-opaque-key-factory';
import { DeepHashedModuleOpaqueKeyFactory } from './opaque-key-factory/deep-hashed-module-opaque-key-factory';
import { ModuleOpaqueKeyFactory } from './opaque-key-factory/interfaces/module-opaque-key-factory.interface';

type ModuleMetatype = Type<any> | DynamicModule | Promise<DynamicModule>;
type ModuleScope = Type<any>[];

export class NestContainer {
private readonly globalModules = new Set<Module>();
private readonly moduleTokenFactory = new ModuleTokenFactory();
private readonly moduleCompiler = new ModuleCompiler(this.moduleTokenFactory);
private readonly modules = new ModulesContainer();
private readonly dynamicModulesMetadata = new Map<
string,
Partial<DynamicModule>
>();
private readonly internalProvidersStorage = new InternalProvidersStorage();
private readonly _serializedGraph = new SerializedGraph();
private moduleCompiler: ModuleCompiler;
private internalCoreModule: Module;

constructor(
private readonly _applicationConfig: ApplicationConfig = undefined,
) {}
private readonly _applicationConfig:
| ApplicationConfig
| undefined = undefined,
private readonly _contextOptions:
| NestApplicationContextOptions
| undefined = undefined,
) {
const moduleOpaqueKeyFactory =
this._contextOptions?.moduleIdGeneratorAlgorithm === 'deep-hash'
? new DeepHashedModuleOpaqueKeyFactory()
: new ByReferenceModuleOpaqueKeyFactory({
keyGenerationStrategy: this._contextOptions?.snapshot
? 'shallow'
: 'random',
});
this.moduleCompiler = new ModuleCompiler(moduleOpaqueKeyFactory);
}

get serializedGraph(): SerializedGraph {
return this._serializedGraph;
Expand Down Expand Up @@ -321,8 +338,8 @@ export class NestContainer {
this.modules[InternalCoreModule.name] = moduleRef;
}

public getModuleTokenFactory(): ModuleTokenFactory {
return this.moduleTokenFactory;
public getModuleTokenFactory(): ModuleOpaqueKeyFactory {
return this.moduleCompiler.moduleOpaqueKeyFactory;
}

public registerRequestProvider<T = any>(request: T, contextId: ContextId) {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/injector/module.ts
Expand Up @@ -653,7 +653,12 @@ export class Module {

private generateUuid(): string {
const prefix = 'M_';
const key = this.name?.toString() ?? this.token?.toString();
const key = this.token
? this.token.includes(':')
? this.token.split(':')[1]
: this.token
: this.name;

return key ? UuidFactory.get(`${prefix}_${key}`) : randomStringGenerator();
}
}
@@ -0,0 +1,63 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { createHash } from 'crypto';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';

const K_MODULE_ID = Symbol('K_MODULE_ID');

export class ByReferenceModuleOpaqueKeyFactory
implements ModuleOpaqueKeyFactory
{
private readonly keyGenerationStrategy: 'random' | 'shallow';

constructor(options?: { keyGenerationStrategy: 'random' | 'shallow' }) {
this.keyGenerationStrategy = options?.keyGenerationStrategy ?? 'random';
}

public createForStatic(
moduleCls: Type,
originalRef: Type | ForwardReference = moduleCls,
): string {
return this.getOrCreateModuleId(moduleCls, undefined, originalRef);
}

public createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule>,
originalRef: DynamicModule | ForwardReference,
): string {
return this.getOrCreateModuleId(moduleCls, dynamicMetadata, originalRef);
}

private getOrCreateModuleId(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule> | undefined,
originalRef: Type | DynamicModule | ForwardReference,
): string {
if (originalRef[K_MODULE_ID]) {
return originalRef[K_MODULE_ID];
}

let moduleId: string;
if (this.keyGenerationStrategy === 'random') {
moduleId = this.generateRandomString();
} else {
moduleId = dynamicMetadata
? `${this.generateRandomString()}:${this.hashString(moduleCls.name + JSON.stringify(dynamicMetadata))}`
: `${this.generateRandomString()}:${this.hashString(moduleCls.toString())}`;
}

originalRef[K_MODULE_ID] = moduleId;
return moduleId;
}

private hashString(value: string): string {
return createHash('sha256').update(value).digest('hex');
}

private generateRandomString(): string {
return randomStringGenerator();
}
}
@@ -1,34 +1,48 @@
import { DynamicModule, Logger } from '@nestjs/common';
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
import { Logger } from '@nestjs/common/services/logger.service';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { isFunction, isSymbol } from '@nestjs/common/utils/shared.utils';
import { createHash } from 'crypto';
import stringify from 'fast-safe-stringify';
import { performance } from 'perf_hooks';
import { ModuleOpaqueKeyFactory } from './interfaces/module-opaque-key-factory.interface';

const CLASS_STR = 'class ';
const CLASS_STR_LEN = CLASS_STR.length;

export class ModuleTokenFactory {
private readonly moduleTokenCache = new Map<string, string>();
export class DeepHashedModuleOpaqueKeyFactory
implements ModuleOpaqueKeyFactory
{
private readonly moduleIdsCache = new WeakMap<Type<unknown>, string>();
private readonly logger = new Logger(ModuleTokenFactory.name, {
private readonly moduleTokenCache = new Map<string, string>();
private readonly logger = new Logger(DeepHashedModuleOpaqueKeyFactory.name, {
timestamp: true,
});

public create(
metatype: Type<unknown>,
dynamicModuleMetadata?: Partial<DynamicModule> | undefined,
): string {
const moduleId = this.getModuleId(metatype);
public createForStatic(moduleCls: Type): string {
const moduleId = this.getModuleId(moduleCls);
const moduleName = this.getModuleName(moduleCls);

if (!dynamicModuleMetadata) {
return this.getStaticModuleToken(moduleId, this.getModuleName(metatype));
const key = `${moduleId}_${moduleName}`;
if (this.moduleTokenCache.has(key)) {
return this.moduleTokenCache.get(key);
}

const hash = this.hashString(key);
this.moduleTokenCache.set(key, hash);
return hash;
}

public createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule>,
): string {
const moduleId = this.getModuleId(moduleCls);
const moduleName = this.getModuleName(moduleCls);
const opaqueToken = {
id: moduleId,
module: this.getModuleName(metatype),
dynamic: dynamicModuleMetadata,
module: moduleName,
dynamic: dynamicMetadata,
};
const start = performance.now();
const opaqueTokenString = this.getStringifiedOpaqueToken(opaqueToken);
Expand All @@ -44,17 +58,6 @@ export class ModuleTokenFactory {
return this.hashString(opaqueTokenString);
}

public getStaticModuleToken(moduleId: string, moduleName: string): string {
const key = `${moduleId}_${moduleName}`;
if (this.moduleTokenCache.has(key)) {
return this.moduleTokenCache.get(key);
}

const hash = this.hashString(key);
this.moduleTokenCache.set(key, hash);
return hash;
}

public getStringifiedOpaqueToken(opaqueToken: object | undefined): string {
// Uses safeStringify instead of JSON.stringify to support circular dynamic modules
// The replacer function is also required in order to obtain real class names
Expand Down
@@ -0,0 +1,26 @@
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';

export interface ModuleOpaqueKeyFactory {
/**
* Creates a unique opaque key for the given static module.
* @param moduleCls A static module class.
* @param originalRef Original object reference. In most cases, it's the same as `moduleCls`.
*/
createForStatic(
moduleCls: Type,
originalRef: Type | ForwardReference,
): string;
/**
* Creates a unique opaque key for the given dynamic module.
* @param moduleCls A dynamic module class reference.
* @param dynamicMetadata Partial dynamic module metadata.
* @param originalRef Original object reference.
*/
createForDynamic(
moduleCls: Type<unknown>,
dynamicMetadata: Partial<DynamicModule>,
originalRef: DynamicModule | ForwardReference,
): string;
}
11 changes: 9 additions & 2 deletions packages/core/nest-application-context.ts
Expand Up @@ -50,7 +50,7 @@ export class NestApplicationContext<

private shouldFlushLogsOnOverride = false;
private readonly activeShutdownSignals = new Array<string>();
private readonly moduleCompiler = new ModuleCompiler();
private readonly moduleCompiler: ModuleCompiler;
private shutdownCleanupRef?: (...args: unknown[]) => unknown;
private _instanceLinksHost: InstanceLinksHost;
private _moduleRefsForHooksByDistance?: Array<Module>;
Expand All @@ -70,6 +70,7 @@ export class NestApplicationContext<
) {
super();
this.injector = new Injector();
this.moduleCompiler = container.getModuleCompiler();

if (this.appOptions.preview) {
this.printInPreviewModeWarning();
Expand All @@ -95,7 +96,13 @@ export class NestApplicationContext<
const moduleTokenFactory = this.container.getModuleTokenFactory();
const { type, dynamicMetadata } =
this.moduleCompiler.extractMetadata(moduleType);
const token = moduleTokenFactory.create(type, dynamicMetadata);
const token = dynamicMetadata
? moduleTokenFactory.createForDynamic(
type,
dynamicMetadata,
moduleType as DynamicModule,
)
: moduleTokenFactory.createForStatic(type, moduleType as Type);

const selectedModule = modulesContainer.get(token);
if (!selectedModule) {
Expand Down