Skip to content

Commit

Permalink
chore: use GlobalGraph to replace AppGraph/ModuleGraph (#242)
Browse files Browse the repository at this point in the history
Let MultiInstanceProto be first class. In future:
- impl loadProtoDesc for loader, not add proto in runtime
- impl strict mode, check proto exists before boot
- impl GlobalGraph dump/restore

<!--
Thank you for your pull request. Please review below requirements.
Bug fixes and new features should include tests and possibly benchmarks.
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md

感谢您贡献代码。请确认下列 checklist 的完成情况。
Bug 修复和新功能必须包含测试,必要时请附上性能测试。
Contributors guide:
https://github.com/eggjs/egg/blob/master/CONTRIBUTING.md
-->

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to
[x]. -->

- [ ] `npm test` passes
- [ ] tests and/or benchmarks are included
- [ ] documentation is changed or added
- [ ] commit message follows commit guidelines

##### Affected core subsystem(s)
<!-- Provide affected core subsystem(s). -->


##### Description of change
<!-- Provide a description of the change below this comment. -->

<!--
- any feature?
- close https://github.com/eggjs/egg/ISSUE_URL
-->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Introduced new interfaces, `ModuleDumpOptions` and
`EggModuleLoaderOptions`, for enhanced configuration.
- Added methods for stringifying module descriptors and improved error
logging during module loading.
- Added new methods for managing multi-instance prototypes and enhanced
prototype descriptor functionalities.

- **Improvements**
- Streamlined graph generation and loading processes by consolidating
logic around the `GlobalGraph`.
- Enhanced the `Runner` class to support better logging and directory
management.
- Updated `LoaderUtil` with new functions for building module nodes and
global graphs.

- **Bug Fixes**
- Improved error handling in module loading and descriptor dumping
processes.

- **Documentation**
- Expanded public API with new exports for module and graph-related
functionalities.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
killagu authored Oct 7, 2024
1 parent 4333b21 commit 8f025d5
Show file tree
Hide file tree
Showing 36 changed files with 1,371 additions and 196 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,4 @@ plugin/tegg/test/fixtures/apps/**/*.js
!plugin/tegg/test/fixtures/**/node_modules
!plugin/config/test/fixtures/**/node_modules
.node
.egg
87 changes: 55 additions & 32 deletions core/common-util/src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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;
}
}
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)) {
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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) {
Expand Down
47 changes: 46 additions & 1 deletion core/core-decorator/src/util/PrototypeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
InjectConstructorInfo,
InjectObjectInfo,
InjectType, LoadUnitNameQualifierAttribute,
MultiInstancePrototypeGetObjectsContext, QualifierAttribute,
MultiInstancePrototypeGetObjectsContext,
MultiInstanceType,
QualifierAttribute,
} from '@eggjs/tegg-types';
import { MetadataUtil } from './MetadataUtil';

Expand Down Expand Up @@ -57,6 +59,21 @@ export class PrototypeUtil {
return MetadataUtil.getBooleanMetaData(PrototypeUtil.IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE, clazz);
}

/**
* Get the type of the egg multi-instance prototype.
* @param {Function} clazz -
*/
static getEggMultiInstancePrototypeType(clazz: EggProtoImplClass): MultiInstanceType | undefined {
if (!PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
return;
}
const metadata = MetadataUtil.getMetaData<EggMultiInstancePrototypeInfo>(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz);
if (metadata) {
return MultiInstanceType.STATIC;
}
return MultiInstanceType.DYNAMIC;
}

/**
* set class file path
* @param {Function} clazz -
Expand Down Expand Up @@ -129,6 +146,33 @@ export class PrototypeUtil {
MetadataUtil.defineMetaData(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, property, clazz);
}

/**
* Get instance property of Static multi-instance prototype.
* @param {EggProtoImplClass} clazz -
*/
static getStaticMultiInstanceProperty(clazz: EggProtoImplClass): EggMultiInstancePrototypeInfo | undefined {
const metadata = MetadataUtil.getMetaData<EggMultiInstancePrototypeInfo>(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_STATIC_PROPERTY, clazz);
if (metadata) {
return metadata;
}
}

/**
* Get instance property of Dynamic multi-instance prototype.
* @param {EggProtoImplClass} clazz -
* @param {MultiInstancePrototypeGetObjectsContext} ctx -
*/
static getDynamicMultiInstanceProperty(clazz: EggProtoImplClass, ctx: MultiInstancePrototypeGetObjectsContext): EggMultiInstancePrototypeInfo | undefined {
const callBackMetadata = MetadataUtil.getMetaData<EggMultiInstanceCallbackPrototypeInfo>(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, clazz);
if (callBackMetadata) {
const objects = callBackMetadata.getObjects(ctx);
return {
...callBackMetadata,
objects,
};
}
}

/**
* get class property
* @param {EggProtoImplClass} clazz -
Expand All @@ -142,6 +186,7 @@ export class PrototypeUtil {
const callBackMetadata = MetadataUtil.getMetaData<EggMultiInstanceCallbackPrototypeInfo>(PrototypeUtil.MULTI_INSTANCE_PROTOTYPE_CALLBACK_PROPERTY, clazz);
if (callBackMetadata) {
const objects = callBackMetadata.getObjects(ctx);
// TODO delete in next major version, default qualifier be added in ProtoDescriptorHelper.addDefaultQualifier
const defaultQualifier = [{
attribute: InitTypeQualifierAttribute,
value: callBackMetadata.initType,
Expand Down
14 changes: 14 additions & 0 deletions core/core-decorator/src/util/QualifierUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,18 @@ export class QualifierUtil {
const qualifiers = properQualifiers?.get(property);
return qualifiers?.get(attribute);
}

static matchQualifiers(clazzQualifiers: QualifierInfo[], requestQualifiers: QualifierInfo[]): boolean {
for (const request of requestQualifiers) {
if (!clazzQualifiers.find(t => t.attribute === request.attribute && t.value === request.value)) {
return false;
}
}
return true;
}

static equalQualifiers(clazzQualifiers: QualifierInfo[], requestQualifiers: QualifierInfo[]): boolean {
if (clazzQualifiers.length !== requestQualifiers.length) return false;
return QualifierUtil.matchQualifiers(clazzQualifiers, requestQualifiers);
}
}
30 changes: 29 additions & 1 deletion core/loader/src/LoaderFactory.ts
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;

Expand All @@ -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[] = [];
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();
for (const clazz of clazzList) {
if (PrototypeUtil.isEggPrototype(clazz)) {
res.clazzList.push(clazz);
} else if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) {
res.multiInstanceClazzList.push(clazz);
}
}
}
return result;
}
}
9 changes: 9 additions & 0 deletions core/metadata/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@ export * from './src/util/ClassUtil';
export * from './src/impl/LoadUnitMultiInstanceProtoHook';
export * from './src/model/AppGraph';

export * from './src/model/graph/GlobalGraph';
export * from './src/model/graph/GlobalModuleNode';
export * from './src/model/graph/GlobalModuleNodeBuilder';
export * from './src/model/graph/ProtoNode';
export * from './src/model/graph/ProtoSelector';
export * from './src/model/ProtoDescriptor/AbstractProtoDescriptor';
export * from './src/model/ProtoDescriptor/ClassProtoDescriptor';
export * from './src/model/ModuleDescriptor';

import './src/impl/ModuleLoadUnit';
import './src/impl/EggPrototypeBuilder';
21 changes: 21 additions & 0 deletions core/metadata/src/factory/EggPrototypeCreatorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
EggPrototypeLifecycleContext,
} from '@eggjs/tegg-types';
import { EggPrototypeLifecycleUtil } from '../model/EggPrototype';
import { ClassProtoDescriptor } from '../model/ProtoDescriptor/ClassProtoDescriptor';

export class EggPrototypeCreatorFactory {
private static creatorMap = new Map<string, EggPrototypeCreator>();
Expand Down Expand Up @@ -81,4 +82,24 @@ export class EggPrototypeCreatorFactory {
return protos;

}

static async createProtoByDescriptor(protoDescriptor: ClassProtoDescriptor, loadUnit: LoadUnit): Promise<EggPrototype> {
const creator = this.getPrototypeCreator(protoDescriptor.protoImplType);
if (!creator) {
throw new Error(`not found proto creator for type: ${protoDescriptor.protoImplType}`);
}
const ctx: EggPrototypeLifecycleContext = {
clazz: protoDescriptor.clazz,
loadUnit,
prototypeInfo: protoDescriptor,
};
const proto = creator(ctx);
await EggPrototypeLifecycleUtil.objectPreCreate(ctx, proto);
if (proto.init) {
await proto.init(ctx);
}
await EggPrototypeLifecycleUtil.objectPostCreate(ctx, proto);
PrototypeUtil.setClazzProto(protoDescriptor.clazz, proto);
return proto;
}
}
4 changes: 1 addition & 3 deletions core/metadata/src/impl/EggPrototypeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class EggPrototypeBuilder {
private injectObjects: Array<InjectObject | InjectConstructor> = [];
private loadUnit: LoadUnit;
private qualifiers: QualifierInfo[] = [];
private properQualifiers: Record<string, QualifierInfo[]> = {};
private properQualifiers: Record<PropertyKey, QualifierInfo[]> = {};
private className?: string;
private multiInstanceConstructorIndex?: number;
private multiInstanceConstructorAttributes?: QualifierAttribute[];
Expand All @@ -57,7 +57,6 @@ export class EggPrototypeBuilder {
...QualifierUtil.getProtoQualifiers(clazz),
...(ctx.prototypeInfo.qualifiers ?? []),
];
console.log('proto: ', ctx.prototypeInfo.properQualifiers);
builder.properQualifiers = ctx.prototypeInfo.properQualifiers ?? {};
builder.multiInstanceConstructorIndex = PrototypeUtil.getMultiInstanceConstructorIndex(clazz);
builder.multiInstanceConstructorAttributes = PrototypeUtil.getMultiInstanceConstructorAttributes(clazz);
Expand All @@ -67,7 +66,6 @@ export class EggPrototypeBuilder {
private tryFindDefaultPrototype(injectObject: InjectObject): EggPrototype {
const propertyQualifiers = QualifierUtil.getProperQualifiers(this.clazz, injectObject.refName);
const multiInstancePropertyQualifiers = this.properQualifiers[injectObject.refName as string] ?? [];
console.log('multi instance: ', this.properQualifiers, injectObject.refName);
return EggPrototypeFactory.instance.getPrototype(injectObject.objName, this.loadUnit, [
...propertyQualifiers,
...multiInstancePropertyQualifiers,
Expand Down
Loading

0 comments on commit 8f025d5

Please sign in to comment.