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: impl MultiInstance inject MultiInstance #240

Merged
merged 1 commit into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions core/metadata/src/impl/LoadUnitMultiInstanceProtoHook.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { PrototypeUtil } from '@eggjs/core-decorator';
import type { EggProtoImplClass, LifecycleHook, LoadUnit, LoadUnitLifecycleContext } from '@eggjs/tegg-types';
import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory';
import { EggPrototypeFactory } from '../factory/EggPrototypeFactory';
// import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory';
// import { EggPrototypeFactory } from '../factory/EggPrototypeFactory';
killagu marked this conversation as resolved.
Show resolved Hide resolved

export class LoadUnitMultiInstanceProtoHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
multiInstanceClazzSet: Set<EggProtoImplClass> = new Set();
static multiInstanceClazzSet: Set<EggProtoImplClass> = new Set();

async preCreate(ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
const clazzList = ctx.loader.load();
const multiInstanceClazzList = Array.from(this.multiInstanceClazzSet);
static setAllClassList(clazzList: readonly EggProtoImplClass[]) {
for (const clazz of clazzList) {
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
this.multiInstanceClazzSet.add(clazz);
}
Comment on lines +9 to 13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace 'this' with class name in static method for clarity.

Using this in static methods can be confusing because this refers to the class itself. It's clearer to reference the class name directly. Replace this.multiInstanceClazzSet with LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet to improve readability.

Apply this diff to enhance clarity:

static setAllClassList(clazzList: readonly EggProtoImplClass[]) {
  for (const clazz of clazzList) {
    if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
-     this.multiInstanceClazzSet.add(clazz);
+     LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet.add(clazz);
    }
  }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static setAllClassList(clazzList: readonly EggProtoImplClass[]) {
for (const clazz of clazzList) {
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
this.multiInstanceClazzSet.add(clazz);
}
static setAllClassList(clazzList: readonly EggProtoImplClass[]) {
for (const clazz of clazzList) {
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet.add(clazz);
}
🧰 Tools
🪛 Biome

[error] 12-12: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)

}
for (const clazz of multiInstanceClazzList) {
const protos = await EggPrototypeCreatorFactory.createProto(clazz, loadUnit);
for (const proto of protos) {
EggPrototypeFactory.instance.registerPrototype(proto, loadUnit);
}
}
}

static clear() {
this.multiInstanceClazzSet.clear();
}
Comment on lines +17 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace 'this' with class name in static method for clarity.

Similarly, in the clear method, replacing this with the class name makes it explicit and avoids confusion. Change this.multiInstanceClazzSet to LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet.

Apply this diff to enhance clarity:

static clear() {
-  this.multiInstanceClazzSet.clear();
+  LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet.clear();
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static clear() {
this.multiInstanceClazzSet.clear();
}
static clear() {
LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet.clear();
}
🧰 Tools
🪛 Biome

[error] 18-18: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

(lint/complexity/noThisInStatic)


async preCreate(): Promise<void> {
// ...
}
}

149 changes: 128 additions & 21 deletions core/metadata/src/impl/ModuleLoadUnit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EggPrototypeFactory } from '../factory/EggPrototypeFactory';
import { LoadUnitFactory } from '../factory/LoadUnitFactory';
import { EggPrototypeCreatorFactory } from '../factory/EggPrototypeCreatorFactory';
import { MultiPrototypeFound } from '../errors';
import { LoadUnitMultiInstanceProtoHook } from './LoadUnitMultiInstanceProtoHook';

let id = 0;

Expand Down Expand Up @@ -64,21 +65,77 @@ class ProtoNode implements GraphNodeObj {
}
}

class MultiInstanceProtoNode implements GraphNodeObj {
readonly clazz: EggProtoImplClass;
readonly name: EggPrototypeName;
readonly id: string;
readonly qualifiers: QualifierInfo[];
readonly initType: ObjectInitTypeLike;
readonly unitPath: string;
readonly moduleName: string;

constructor(clazz: EggProtoImplClass, objName: EggPrototypeName, unitPath: string, moduleName: string) {
this.name = objName;
this.id = '' + (id++);
this.clazz = clazz;
this.qualifiers = QualifierUtil.getProtoQualifiers(clazz);
this.initType = PrototypeUtil.getInitType(clazz, {
unitPath,
moduleName,
})!;
this.unitPath = unitPath;
this.moduleName = moduleName;
}

verifyQualifiers(qualifiers: QualifierInfo[]): boolean {
const property = PrototypeUtil.getMultiInstanceProperty(this.clazz, {
unitPath: this.unitPath,
moduleName: this.moduleName,
});
if (!property) {
return false;
}
for (const obj of property.objects) {
const selfQualifiers = [
...this.qualifiers,
...obj.qualifiers,
];
if (this.verifyInstanceQualifiers(selfQualifiers, qualifiers)) {
return true;
}
}
return false;
}

verifyInstanceQualifiers(selfQualifiers: QualifierInfo[], qualifiers: QualifierInfo[]): boolean {
for (const qualifier of qualifiers) {
if (!selfQualifiers.find(t => t.attribute === qualifier.attribute && t.value === qualifier.value)) {
return false;
}
}
return true;
}

toString(): string {
return `${this.clazz.name}@${PrototypeUtil.getFilePath(this.clazz)}`;
}
}

Comment on lines +68 to +123
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring to reduce code duplication between ProtoNode and MultiInstanceProtoNode

The classes ProtoNode and MultiInstanceProtoNode share several properties and methods, such as clazz, name, id, qualifiers, initType, and the toString() method. To enhance maintainability and reduce code duplication, consider extracting the shared functionality into a base class or interface that both classes can extend or implement.

export class ModuleGraph {
private graph: Graph<ProtoNode>;
private graph: Graph<ProtoNode | MultiInstanceProtoNode>;
clazzList: EggProtoImplClass[];
readonly unitPath: string;
readonly name: string;

constructor(clazzList: EggProtoImplClass[], unitPath: string, name: string) {
this.clazzList = clazzList;
this.graph = new Graph<ProtoNode>();
this.graph = new Graph<ProtoNode | MultiInstanceProtoNode>();
this.unitPath = unitPath;
this.name = name;
this.build();
}

private findInjectNode(objName: EggPrototypeName, qualifiers: QualifierInfo[], parentInitTye: ObjectInitTypeLike): GraphNode<ProtoNode> | undefined {
private findInjectNode(objName: EggPrototypeName, qualifiers: QualifierInfo[], parentInitTye: ObjectInitTypeLike): GraphNode<ProtoNode | MultiInstanceProtoNode> | undefined {
let nodes = Array.from(this.graph.nodes.values())
.filter(t => t.val.name === objName)
.filter(t => t.val.verifyQualifiers(qualifiers));
Expand All @@ -94,7 +151,16 @@ export class ModuleGraph {
value: parentInitTye,
};

nodes = nodes.filter(t => t.val.verifyQualifier(initTypeQualifier));
nodes = nodes.filter(t => t.val.verifyQualifiers([ initTypeQualifier ]));
if (nodes.length === 1) {
return nodes[0];
}

const temp: Map<EggProtoImplClass, GraphNode<ProtoNode | MultiInstanceProtoNode>> = new Map();
for (const node of nodes) {
temp.set(node.val.clazz, node);
}
nodes = Array.from(temp.values());
if (nodes.length === 1) {
return nodes[0];
}
Expand All @@ -104,14 +170,18 @@ export class ModuleGraph {
}

private build() {
const protoGraphNodes: GraphNode<ProtoNode>[] = [];
const protoGraphNodes: GraphNode<ProtoNode | MultiInstanceProtoNode>[] = [];
for (const clazz of this.clazzList) {
const objNames = PrototypeUtil.getObjNames(clazz, {
unitPath: this.unitPath,
moduleName: this.name,
});
for (const objName of objNames) {
protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath, this.name)));
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
protoGraphNodes.push(new GraphNode(new MultiInstanceProtoNode(clazz, objName, this.unitPath, this.name)));
} else {
protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath, this.name)));
}
Comment on lines +173 to +184
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify node creation logic in the build method

In the build method, the logic for creating GraphNode instances checks whether each class is a multi-instance prototype to decide which node type to instantiate. Consider refactoring this section to reduce complexity, possibly by using a factory pattern or a common constructor that handles the differentiation internally.

}
}
for (const node of protoGraphNodes) {
Expand All @@ -120,13 +190,34 @@ export class ModuleGraph {
}
}
for (const node of protoGraphNodes) {
const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz);
for (const injectObject of injectObjects) {
const qualifiers = QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName);
const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType);
// If not found maybe in other module
if (injectNode) {
this.graph.addEdge(node, injectNode);
if (PrototypeUtil.isEggMultiInstancePrototype(node.val.clazz)) {
const property = PrototypeUtil.getMultiInstanceProperty(node.val.clazz, {
moduleName: this.name,
unitPath: this.unitPath,
});
for (const objectInfo of property?.objects || []) {
const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz);
for (const injectObject of injectObjects) {
const qualifiers = [
...QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName),
...objectInfo.properQualifiers?.[injectObject.refName] ?? [],
];
const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType);
// If not found maybe in other module
if (injectNode) {
this.graph.addEdge(node, injectNode);
}
}
}
} else {
const injectObjects = PrototypeUtil.getInjectObjects(node.val.clazz);
for (const injectObject of injectObjects) {
const qualifiers = QualifierUtil.getProperQualifiers(node.val.clazz, injectObject.refName);
const injectNode = this.findInjectNode(injectObject.objName, qualifiers, node.val.initType);
// If not found maybe in other module
if (injectNode) {
this.graph.addEdge(node, injectNode);
}
Comment on lines +193 to +220
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Eliminate code duplication when adding graph edges

The process of adding edges to the graph for dependency resolution is similar for both multi-instance prototypes and regular prototypes. To improve readability and reduce maintenance effort, consider refactoring the common logic into a shared helper function or method.

}
}
}
Expand Down Expand Up @@ -162,8 +253,9 @@ export class ModuleLoadUnit implements LoadUnit {
this.loader = loader;
}

private loadClazz(): EggProtoImplClass[] {
private doLoadClazz(): EggProtoImplClass[] {
const clazzList = this.loader.load();
const result = clazzList.slice();
for (const clazz of clazzList) {
const defaultQualifier = [{
attribute: InitTypeQualifierAttribute,
Expand All @@ -179,12 +271,30 @@ export class ModuleLoadUnit implements LoadUnit {
QualifierUtil.addProtoQualifier(clazz, qualifier.attribute, qualifier.value);
});
}
return clazzList;
for (const clazz of LoadUnitMultiInstanceProtoHook.multiInstanceClazzSet) {
const instanceProperty = PrototypeUtil.getMultiInstanceProperty(clazz, {
moduleName: this.name,
unitPath: this.unitPath,
});
if (instanceProperty) {
result.push(clazz);
}
}
return result;
}

private loadClazz() {
if (!this.clazzList) {
const clazzList = this.doLoadClazz();
const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name);
protoGraph.sort();
this.clazzList = protoGraph.clazzList;
}
Comment on lines +285 to +292
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure thread safety in loadClazz method

The loadClazz method checks if this.clazzList is uninitialized before loading classes. If loadClazz can be called concurrently, consider adding synchronization mechanisms to prevent race conditions and ensure that this.clazzList is correctly initialized before use.

}

async preLoad() {
const clazzList = this.loader.load();
for (const protoClass of clazzList) {
this.loadClazz();
for (const protoClass of this.clazzList) {
const fnName = LifecycleUtil.getStaticLifecycleHook('preLoad', protoClass);
if (fnName) {
await protoClass[fnName]?.();
Expand All @@ -193,10 +303,7 @@ export class ModuleLoadUnit implements LoadUnit {
}

async init() {
const clazzList = this.loadClazz();
const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name);
protoGraph.sort();
this.clazzList = protoGraph.clazzList;
this.loadClazz();
for (const clazz of this.clazzList) {
const protos = await EggPrototypeCreatorFactory.createProto(clazz, this);
for (const proto of protos) {
Expand Down
54 changes: 46 additions & 8 deletions core/metadata/src/model/AppGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { EggProtoImplClass, EggPrototypeName, GraphNodeObj, ModuleReference
export interface InstanceClazzMeta {
name: PropertyKey;
qualifiers: QualifierInfo[];
properQualifiers: Record<PropertyKey, QualifierInfo[]>;
accessLevel: AccessLevel,
instanceModule: GraphNode<ModuleNode>;
ownerModule: GraphNode<ModuleNode>;
Expand Down Expand Up @@ -65,6 +66,7 @@ export class ClazzMap {
...qualifiers,
...info.qualifiers,
],
properQualifiers: info.properQualifiers || {},
instanceModule: instanceNode,
ownerModule: ownerNode,
});
Expand All @@ -81,6 +83,7 @@ export class ClazzMap {
moduleName: ownerNode.val.moduleConfig.name,
}) as AccessLevel,
qualifiers,
properQualifiers: {},
ownerModule: ownerNode,
instanceModule: ownerNode,
});
Expand Down Expand Up @@ -153,7 +156,7 @@ export class ClazzMap {

for (const obj of mayObjs) {
result.add(obj.instanceModule);
result.add(obj.ownerModule);
// result.add(obj.ownerModule);
}
return Array.from(result);
}
Expand Down Expand Up @@ -215,6 +218,16 @@ export class AppGraph {
}
}

getClazzList(): readonly EggProtoImplClass[] {
const clazzSet = new Set<EggProtoImplClass>();
for (const node of this.graph.nodes.values()) {
for (const clazz of node.val.getClazzList()) {
clazzSet.add(clazz);
}
}
return Array.from(clazzSet);
}

build() {
this.clazzMap = new ClazzMap(this.graph);

Expand All @@ -225,13 +238,38 @@ export class AppGraph {
const injectObjects = PrototypeUtil.getInjectObjects(clazz);
// 3. iterate all inject objects
for (const injectObject of injectObjects) {
const properQualifiers = QualifierUtil.getProperQualifiers(clazz, injectObject.refName);
// 4. find dependency module
const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node);
for (const moduleNode of dependencyModules) {
// 5. add edge
if (node !== moduleNode) {
this.graph.addEdge(node, moduleNode);
if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
for (const instanceNode of this.graph.nodes.values()) {
const property = PrototypeUtil.getMultiInstanceProperty(clazz, {
unitPath: instanceNode.val.moduleConfig.path,
Comment on lines +243 to +244
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add assertion to ensure property is defined

To prevent potential runtime errors, consider adding an assertion after retrieving property to ensure it is not undefined. This follows the pattern used elsewhere and ensures that the code fails fast if property is missing.

Apply this diff to add the assertion:

 for (const instanceNode of this.graph.nodes.values()) {
   const property = PrototypeUtil.getMultiInstanceProperty(clazz, {
     unitPath: instanceNode.val.moduleConfig.path,
     moduleName: instanceNode.val.moduleConfig.name,
   });
+  assert(property, `multi instance property not found for ${clazz.name}`);
   for (const info of property?.objects || []) {

Committable suggestion was skipped due to low confidence.

moduleName: instanceNode.val.moduleConfig.name,
});
for (const info of property?.objects || []) {
const properQualifiers = [
...QualifierUtil.getProperQualifiers(clazz, injectObject.refName),
...info.properQualifiers?.[injectObject.refName] ?? [],
];
// 4. find dependency module
const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node);
for (const moduleNode of dependencyModules) {
// 5. add edge
if (instanceNode !== moduleNode) {
this.graph.addEdge(instanceNode, moduleNode);
}
}
}
}
} else {
const properQualifiers = [
...QualifierUtil.getProperQualifiers(clazz, injectObject.refName),
];
// 4. find dependency module
const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node);
for (const moduleNode of dependencyModules) {
// 5. add edge
if (node !== moduleNode) {
this.graph.addEdge(node, moduleNode);
}
Comment on lines +241 to +272
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor duplicated code in dependency module processing

The logic for finding dependency modules and adding graph edges is duplicated in both the if and else blocks for multi-instance and regular prototypes. Consider refactoring this shared logic into a separate method to enhance code maintainability and reduce duplication.

For example, you could extract the dependency processing into a method like processDependencies:

private processDependencies(node: GraphNode<ModuleNode>, instanceNode: GraphNode<ModuleNode>, properQualifiers: QualifierInfo[], injectObject: InjectObject) {
  const dependencyModules = this.clazzMap.findDependencyModule(injectObject.objName, properQualifiers, node);
  for (const moduleNode of dependencyModules) {
    if (instanceNode !== moduleNode) {
      this.graph.addEdge(instanceNode, moduleNode);
    }
  }
}

Then call this method from both branches, passing the appropriate parameters.

}
}
}
Expand Down
Loading
Loading