-
Notifications
You must be signed in to change notification settings - Fork 38
chore: use GlobalGraph to replace AppGraph/ModuleGraph #242
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,10 +2,15 @@ import type { GraphNodeObj } from '@eggjs/tegg-types'; | |
|
|
||
| const inspect = Symbol.for('nodejs.util.inspect.custom'); | ||
|
|
||
| export class GraphNode<T extends GraphNodeObj> { | ||
| export interface EdgeMeta { | ||
| equal(meta: EdgeMeta): boolean; | ||
| toString(): string; | ||
| } | ||
|
|
||
| export class GraphNode<T extends GraphNodeObj, M extends EdgeMeta = EdgeMeta> { | ||
| val: T; | ||
| toNodeMap: Map<string, GraphNode<T>> = new Map(); | ||
| fromNodeMap: Map<string, GraphNode<T>> = new Map(); | ||
| toNodeMap: Map<string, {node: GraphNode<T, M>, meta?: M}> = new Map(); | ||
| fromNodeMap: Map<string, {node: GraphNode<T, M>, meta?: M}> = new Map(); | ||
|
|
||
| constructor(val: T) { | ||
| this.val = val; | ||
|
|
@@ -15,19 +20,19 @@ export class GraphNode<T extends GraphNodeObj> { | |
| return this.val.id; | ||
| } | ||
|
|
||
| addToVertex(node: GraphNode<T>) { | ||
| addToVertex(node: GraphNode<T, M>, meta?: M) { | ||
| if (this.toNodeMap.has(node.id)) { | ||
| return false; | ||
| } | ||
| this.toNodeMap.set(node.id, node); | ||
| this.toNodeMap.set(node.id, { node, meta }); | ||
| return true; | ||
| } | ||
|
|
||
| addFromVertex(node: GraphNode<T>) { | ||
| addFromVertex(node: GraphNode<T, M>, meta?: M) { | ||
| if (this.fromNodeMap.has(node.id)) { | ||
| return false; | ||
| } | ||
| this.fromNodeMap.set(node.id, node); | ||
| this.fromNodeMap.set(node.id, { node, meta }); | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -48,69 +53,87 @@ export class GraphNode<T extends GraphNodeObj> { | |
| } | ||
| } | ||
|
|
||
| export class GraphPath<T extends GraphNodeObj> { | ||
| export class GraphPath<T extends GraphNodeObj, M extends EdgeMeta = EdgeMeta> { | ||
| nodeIdMap: Map<string, number> = new Map(); | ||
| nodes: Array<GraphNode<T>> = []; | ||
| nodes: Array<{ node: GraphNode<T, M>, meta?: M }> = []; | ||
|
|
||
| pushVertex(node: GraphNode<T>): boolean { | ||
| pushVertex(node: GraphNode<T, M>, meta?: M): boolean { | ||
| const val = this.nodeIdMap.get(node.id) || 0; | ||
| this.nodeIdMap.set(node.id, val + 1); | ||
| this.nodes.push(node); | ||
| this.nodes.push({ node, meta }); | ||
| return val === 0; | ||
| } | ||
|
|
||
| popVertex() { | ||
| const node = this.nodes.pop(); | ||
| if (node) { | ||
| const val = this.nodeIdMap.get(node.id)!; | ||
| this.nodeIdMap.set(node.id, val - 1); | ||
| const nodeHandler = this.nodes.pop(); | ||
| if (nodeHandler) { | ||
| const val = this.nodeIdMap.get(nodeHandler.node.id)!; | ||
| this.nodeIdMap.set(nodeHandler.node.id, val - 1); | ||
| } | ||
| } | ||
|
|
||
| toString() { | ||
| const res = this.nodes.reduce((p, c) => { | ||
| p.push(c.val.toString()); | ||
| let msg = ''; | ||
| if (c.meta) { | ||
| msg += ` ${c.meta.toString()} -> `; | ||
| } else if (p.length) { | ||
| msg += ' -> '; | ||
| } | ||
| msg += c.node.val.toString(); | ||
| p.push(msg); | ||
| return p; | ||
| }, new Array<string>()); | ||
| return res.join(' -> '); | ||
| return res.join(''); | ||
| } | ||
|
|
||
| [inspect]() { | ||
| return this.toString(); | ||
| } | ||
| } | ||
|
|
||
| export class Graph<T extends GraphNodeObj> { | ||
| nodes: Map<string, GraphNode<T>> = new Map(); | ||
| export class Graph<T extends GraphNodeObj, M extends EdgeMeta = EdgeMeta> { | ||
| nodes: Map<string, GraphNode<T, M>> = new Map(); | ||
|
|
||
| addVertex(node: GraphNode<T>): boolean { | ||
| addVertex(node: GraphNode<T, M>): boolean { | ||
| if (this.nodes.has(node.id)) { | ||
| return false; | ||
| } | ||
| this.nodes.set(node.id, node); | ||
| return true; | ||
| } | ||
|
|
||
| addEdge(from: GraphNode<T>, to: GraphNode<T>): boolean { | ||
| to.addFromVertex(from); | ||
| return from.addToVertex(to); | ||
| addEdge(from: GraphNode<T, M>, to: GraphNode<T, M>, meta?: M): boolean { | ||
| to.addFromVertex(from, meta); | ||
| return from.addToVertex(to, meta); | ||
| } | ||
|
|
||
| findToNode(id: string, meta: M): GraphNode<T, M> | undefined { | ||
| const node = this.nodes.get(id); | ||
| if (!node) return undefined; | ||
| for (const { node: toNode, meta: edgeMeta } of node.toNodeMap.values()) { | ||
| if (edgeMeta && meta.equal(edgeMeta)) { | ||
| return toNode; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| appendVertexToPath(node: GraphNode<T>, accessPath: GraphPath<T>): boolean { | ||
| if (!accessPath.pushVertex(node)) { | ||
| appendVertexToPath(node: GraphNode<T, M>, accessPath: GraphPath<T, M>, meta?: M): boolean { | ||
| if (!accessPath.pushVertex(node, meta)) { | ||
| return false; | ||
| } | ||
| for (const toNode of node.toNodeMap.values()) { | ||
| if (!this.appendVertexToPath(toNode, accessPath)) { | ||
| if (!this.appendVertexToPath(toNode.node, accessPath, toNode.meta)) { | ||
| return false; | ||
|
Comment on lines
+122
to
128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Potential stack overflow due to deep recursion in The for (const toNode of node.toNodeMap.values()) {
if (!this.appendVertexToPath(toNode.node, accessPath, toNode.meta)) {
return false;
}
}In graphs with a large depth or in the presence of cycles (even though cycles are handled), this could lead to a stack overflow. Consider refactoring this recursive approach into an iterative one using a stack or queue to manage traversal, which can improve performance and prevent potential stack issues. |
||
| } | ||
| } | ||
| accessPath.popVertex(); | ||
| return true; | ||
| } | ||
|
|
||
| loopPath(): GraphPath<T> | undefined { | ||
| const accessPath = new GraphPath<T>(); | ||
| loopPath(): GraphPath<T, M> | undefined { | ||
| const accessPath = new GraphPath<T, M>(); | ||
| const nodes = Array.from(this.nodes.values()); | ||
| for (const node of nodes) { | ||
| if (!this.appendVertexToPath(node, accessPath)) { | ||
|
|
@@ -120,7 +143,7 @@ export class Graph<T extends GraphNodeObj> { | |
| return; | ||
| } | ||
|
|
||
| accessNode(node: GraphNode<T>, nodes: Array<GraphNode<T>>, accessed: boolean[], res: Array<GraphNode<T>>) { | ||
| accessNode(node: GraphNode<T, M>, nodes: Array<GraphNode<T, M>>, accessed: boolean[], res: Array<GraphNode<T, M>>) { | ||
| const index = nodes.indexOf(node); | ||
| if (accessed[index]) { | ||
| return; | ||
|
|
@@ -131,7 +154,7 @@ export class Graph<T extends GraphNodeObj> { | |
| return; | ||
| } | ||
| for (const toNode of node.toNodeMap.values()) { | ||
| this.accessNode(toNode, nodes, accessed, res); | ||
| this.accessNode(toNode.node, nodes, accessed, res); | ||
| } | ||
| accessed[nodes.indexOf(node)] = true; | ||
| res.push(node); | ||
|
|
@@ -145,8 +168,8 @@ export class Graph<T extends GraphNodeObj> { | |
| // notice: | ||
| // 1. sort result is not stable | ||
| // 2. graph with loop can not be sort | ||
| sort(): Array<GraphNode<T>> { | ||
| const res: Array<GraphNode<T>> = []; | ||
| sort(): Array<GraphNode<T, M>> { | ||
| const res: Array<GraphNode<T, M>> = []; | ||
| const nodes = Array.from(this.nodes.values()); | ||
| const accessed: boolean[] = []; | ||
| for (let i = 0; i < nodes.length; ++i) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,6 @@ | ||
| import type { EggLoadUnitTypeLike, Loader } from '@eggjs/tegg-types'; | ||
| import { EggLoadUnitType, EggLoadUnitTypeLike, EggProtoImplClass, Loader, ModuleReference } from '@eggjs/tegg-types'; | ||
| import { ModuleDescriptor } from '@eggjs/tegg-metadata'; | ||
| import { PrototypeUtil } from '@eggjs/core-decorator'; | ||
|
|
||
| export type LoaderCreator = (unitPath: string) => Loader; | ||
|
|
||
|
|
@@ -16,4 +18,30 @@ export class LoaderFactory { | |
| static registerLoader(type: EggLoadUnitTypeLike, creator: LoaderCreator) { | ||
| this.loaderCreatorMap.set(type, creator); | ||
| } | ||
|
|
||
| static loadApp(moduleReferences: readonly ModuleReference[]): ModuleDescriptor[] { | ||
| const result: ModuleDescriptor[] = []; | ||
| const multiInstanceClazzList: EggProtoImplClass[] = []; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialize Currently, Apply this diff to fix the issue: - const multiInstanceClazzList: EggProtoImplClass[] = [];
for (const moduleReference of moduleReferences) {
+ const multiInstanceClazzList: EggProtoImplClass[] = [];
|
||
| for (const moduleReference of moduleReferences) { | ||
| const loader = LoaderFactory.createLoader(moduleReference.path, moduleReference.loaderType || EggLoadUnitType.MODULE); | ||
| const res: ModuleDescriptor = { | ||
| name: moduleReference.name, | ||
| unitPath: moduleReference.path, | ||
| clazzList: [], | ||
| protos: [], | ||
| multiInstanceClazzList, | ||
| optional: moduleReference.optional, | ||
| }; | ||
| result.push(res); | ||
| const clazzList = loader.load(); | ||
killagu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| for (const clazz of clazzList) { | ||
| if (PrototypeUtil.isEggPrototype(clazz)) { | ||
| res.clazzList.push(clazz); | ||
| } else if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) { | ||
| res.multiInstanceClazzList.push(clazz); | ||
| } | ||
| } | ||
|
Comment on lines
+38
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle classes that are neither single-instance nor multi-instance prototypes. The current implementation only checks for Consider adding an if (PrototypeUtil.isEggPrototype(clazz)) {
res.clazzList.push(clazz);
} else if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
res.multiInstanceClazzList.push(clazz);
} else {
console.warn(`Unhandled class type: ${clazz.name}`);
} |
||
| } | ||
| return result; | ||
| } | ||
|
Comment on lines
+22
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Refactor the While the method implements the core functionality described in the PR objectives, there are several areas for improvement:
Here's a suggested refactor addressing these points: /**
* Load application modules and return their descriptors.
* @param moduleReferences - An array of module references to load.
* @returns An array of ModuleDescriptor objects representing loaded modules.
*/
static loadApp(moduleReferences: readonly ModuleReference[]): ModuleDescriptor[] {
const result: ModuleDescriptor[] = [];
for (const moduleReference of moduleReferences) {
const multiInstanceClazzList: EggProtoImplClass[] = [];
const loader = LoaderFactory.createLoader(moduleReference.path, moduleReference.loaderType || EggLoadUnitType.MODULE);
const res: ModuleDescriptor = {
name: moduleReference.name,
unitPath: moduleReference.path,
clazzList: [],
protos: [],
multiInstanceClazzList,
optional: moduleReference.optional,
};
result.push(res);
try {
const clazzList = loader.load();
for (const clazz of clazzList) {
if (PrototypeUtil.isEggPrototype(clazz)) {
res.clazzList.push(clazz);
} else if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
res.multiInstanceClazzList.push(clazz);
} else {
console.warn(`Unhandled class type: ${clazz.name}`);
}
}
} catch (error) {
console.error(`Failed to load module at ${moduleReference.path}:`, error);
// Decide whether to continue or throw the error based on your error handling strategy
}
}
return result;
}This refactored version addresses the identified issues and improves the overall robustness of the method. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider supporting multiple edges between the same nodes with different metadata.
Currently,
toNodeMapandfromNodeMapin theGraphNodeclass usenode.idas the key. This design prevents adding multiple edges between the same pair of nodes when themetadata differs. If your intention is to support multiple edges with different metadata between the same nodes, consider modifying the data structures to accommodate this. One approach is to change the maps to use a composite key ofnode.idand a metadata identifier, or to store arrays of edges.