Skip to content
Closed
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
12 changes: 12 additions & 0 deletions tegg/core/common-util/src/Graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ export class Graph<T extends GraphNodeObj, M extends EdgeMeta = EdgeMeta> {
return true;
}

/** Remove a vertex and any edges referencing it. Returns false if absent. */
removeVertex(id: string): boolean {
if (!this.nodes.delete(id)) {
return false;
}
for (const node of this.nodes.values()) {
node.toNodeMap.delete(id);
node.fromNodeMap.delete(id);
}
return true;
}

addEdge(from: GraphNode<T, M>, to: GraphNode<T, M>, meta?: M): boolean {
to.addFromVertex(from, meta);
return from.addToVertex(to, meta);
Expand Down
22 changes: 22 additions & 0 deletions tegg/core/core-decorator/src/decorator/ConditionalOnMissing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { EggProtoImplClass } from '@eggjs/tegg-types';

import { PrototypeUtil } from '../util/PrototypeUtil.ts';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use .js extensions in module specifiers.

As per coding guidelines, Tegg TypeScript code must use ESM-only imports and .js extensions in import/export specifiers. Update the newly added or modified lines to use .js instead of .ts.

  • tegg/core/core-decorator/src/decorator/ConditionalOnMissing.ts#L3-L3: Change ../util/PrototypeUtil.ts to ../util/PrototypeUtil.js.
  • tegg/core/core-decorator/src/decorator/Override.ts#L3-L3: Change ../util/PrototypeUtil.ts to ../util/PrototypeUtil.js.
  • tegg/core/core-decorator/src/decorator/index.ts#L1-L1: Change ./ConditionalOnMissing.ts to ./ConditionalOnMissing.js.
  • tegg/core/core-decorator/src/decorator/index.ts#L12-L12: Change ./Override.ts to ./Override.js.
  • tegg/core/core-decorator/test/decorators.test.ts#L16-L16: Change ../src/index.ts to ../src/index.js.
📍 Affects 4 files
  • tegg/core/core-decorator/src/decorator/ConditionalOnMissing.ts#L3-L3 (this comment)
  • tegg/core/core-decorator/src/decorator/Override.ts#L3-L3
  • tegg/core/core-decorator/src/decorator/index.ts#L1-L1
  • tegg/core/core-decorator/src/decorator/index.ts#L12-L12
  • tegg/core/core-decorator/test/decorators.test.ts#L16-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tegg/core/core-decorator/src/decorator/ConditionalOnMissing.ts` at line 3,
Replace the TypeScript extensions with .js in the affected ESM import
specifiers: PrototypeUtil imports in ConditionalOnMissing.ts and Override.ts,
decorator imports in decorator/index.ts, and the src/index import in
decorators.test.ts. No other changes are needed.

Source: Coding guidelines


/**
* Mark a proto as a conditional default for its name: it is used only when it
* is the sole candidate. If any other proto (plain or `@Override`) provides the
* same name, this one is dropped from the graph (never instantiated) — the
* "use me only if missing" default the framework provides so an app can replace
* it just by declaring its own.
*
* ```ts
* @ConditionalOnMissing()
* @SingletonProto({ name: 'foo' })
* class DefaultFoo {}
* ```
*/
export function ConditionalOnMissing() {
return function (clazz: EggProtoImplClass): void {
PrototypeUtil.setConditionalOnMissing(clazz);
};
}
21 changes: 21 additions & 0 deletions tegg/core/core-decorator/src/decorator/Override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EggProtoImplClass } from '@eggjs/tegg-types';

import { PrototypeUtil } from '../util/PrototypeUtil.ts';

/**
* Mark a proto as an explicit override for its name: it deterministically wins
* over a same-name plain proto or a `@ConditionalOnMissing` default, and the
* losers are dropped from the graph (never instantiated).
*
* Put it next to the proto decorator:
* ```ts
* @Override()
* @SingletonProto({ name: 'foo' })
* class MyFoo {}
* ```
*/
export function Override() {
return function (clazz: EggProtoImplClass): void {
PrototypeUtil.setOverride(clazz);
};
}
2 changes: 2 additions & 0 deletions tegg/core/core-decorator/src/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ConditionalOnMissing.ts';
export * from './ConfigSource.ts';
export * from './ContextProto.ts';
export * from './DefineModuleQualifier.ts';
Expand All @@ -8,6 +9,7 @@ export * from './Inject.ts';
export * from './InnerObjectProto.ts';
export * from './ModuleQualifier.ts';
export * from './MultiInstanceInfo.ts';
export * from './Override.ts';
export * from './MultiInstanceProto.ts';
export * from './Prototype.ts';
export * from './SingletonProto.ts';
20 changes: 20 additions & 0 deletions tegg/core/core-decorator/src/util/PrototypeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { MetadataUtil } from './MetadataUtil.ts';

export class PrototypeUtil {
static readonly IS_EGG_OBJECT_PROTOTYPE: symbol = Symbol.for('EggPrototype#isEggPrototype');
static readonly IS_OVERRIDE: symbol = Symbol.for('EggPrototype#isOverride');
static readonly IS_CONDITIONAL_ON_MISSING: symbol = Symbol.for('EggPrototype#isConditionalOnMissing');
static readonly IS_EGG_OBJECT_MULTI_INSTANCE_PROTOTYPE: symbol = Symbol.for(
'EggPrototype#isEggMultiInstancePrototype',
);
Expand Down Expand Up @@ -58,6 +60,24 @@ export class PrototypeUtil {
return MetadataUtil.getOwnBooleanMetaData(PrototypeUtil.IS_EGG_OBJECT_PROTOTYPE, clazz);
}

/** Mark the proto as an explicit override — it wins over a same-name plain / conditional proto. */
static setOverride(clazz: EggProtoImplClass): void {
MetadataUtil.defineMetaData(PrototypeUtil.IS_OVERRIDE, true, clazz);
}

static isOverride(clazz: EggProtoImplClass): boolean {
return MetadataUtil.getOwnBooleanMetaData(PrototypeUtil.IS_OVERRIDE, clazz);
}

/** Mark the proto as a conditional default — it is dropped when any other proto provides the same name. */
static setConditionalOnMissing(clazz: EggProtoImplClass): void {
MetadataUtil.defineMetaData(PrototypeUtil.IS_CONDITIONAL_ON_MISSING, true, clazz);
}

static isConditionalOnMissing(clazz: EggProtoImplClass): boolean {
return MetadataUtil.getOwnBooleanMetaData(PrototypeUtil.IS_CONDITIONAL_ON_MISSING, clazz);
}

/**
* Mark class is egg object multi instance prototype
* @param {Function} clazz -
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ exports[`should export stable 1`] = `
"PUBLIC": "PUBLIC",
},
"CONSTRUCTOR_QUALIFIER_META_DATA": Symbol(EggPrototype#constructorQualifier),
"ConditionalOnMissing": [Function],
"ConfigSourceQualifier": [Function],
"ConfigSourceQualifierAttribute": Symbol(Qualifier.ConfigSource),
"ContextProto": [Function],
Expand Down Expand Up @@ -54,6 +55,7 @@ exports[`should export stable 1`] = `
"CONTEXT": "CONTEXT",
"SINGLETON": "SINGLETON",
},
"Override": [Function],
"PROPERTY_QUALIFIER_META_DATA": Symbol(EggPrototype#propertyQualifier),
"Prototype": [Function],
"PrototypeUtil": [Function],
Expand Down
24 changes: 23 additions & 1 deletion tegg/core/core-decorator/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import type { EggPrototypeInfo, EggMultiInstancePrototypeInfo, InjectObjectInfo } from '@eggjs/tegg-types';
import { describe, it, expect } from 'vitest';

import { PrototypeUtil, QualifierUtil } from '../src/index.ts';
import { ConditionalOnMissing, Override, PrototypeUtil, QualifierUtil, SingletonProto } from '../src/index.ts';
import CacheService from './fixtures/decators/CacheService.ts';
import {
ChildDynamicMultiInstanceProto,
Expand Down Expand Up @@ -313,4 +313,26 @@ describe('test/decorators.test.ts', () => {
);
});
});

describe('@Override / @ConditionalOnMissing', () => {
it('should mark the proto class', () => {
@Override()
@SingletonProto()
class OverrideProto {}

@ConditionalOnMissing()
@SingletonProto()
class ConditionalProto {}

@SingletonProto()
class PlainProto {}

assert.equal(PrototypeUtil.isOverride(OverrideProto), true);
assert.equal(PrototypeUtil.isConditionalOnMissing(OverrideProto), false);
assert.equal(PrototypeUtil.isConditionalOnMissing(ConditionalProto), true);
assert.equal(PrototypeUtil.isOverride(ConditionalProto), false);
assert.equal(PrototypeUtil.isOverride(PlainProto), false);
assert.equal(PrototypeUtil.isConditionalOnMissing(PlainProto), false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface AbstractProtoDescriptorOptions {
instanceDefineUnitPath: string;
type: ProtoDescriptorTypeLike;
properQualifiers: Record<PropertyKey, QualifierInfo[]>;
override?: boolean;
conditionalOnMissing?: boolean;
}

export abstract class AbstractProtoDescriptor implements ProtoDescriptor {
Expand All @@ -36,6 +38,8 @@ export abstract class AbstractProtoDescriptor implements ProtoDescriptor {
className?: string;
properQualifiers: Record<PropertyKey, QualifierInfo[]>;
type: ProtoDescriptorTypeLike;
override?: boolean;
conditionalOnMissing?: boolean;

protected constructor(options: AbstractProtoDescriptorOptions) {
this.name = options.name;
Expand All @@ -50,6 +54,8 @@ export abstract class AbstractProtoDescriptor implements ProtoDescriptor {
this.instanceDefineUnitPath = options.instanceDefineUnitPath;
this.type = options.type;
this.properQualifiers = options.properQualifiers;
this.override = options.override;
this.conditionalOnMissing = options.conditionalOnMissing;
}

abstract equal(protoDescriptor: ProtoDescriptor): boolean;
Expand Down
2 changes: 2 additions & 0 deletions tegg/core/metadata/src/model/ProtoDescriptorHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export class ProtoDescriptorHelper {
defineModuleName: ctx.defineModuleName || ctx.moduleName,
clazz,
properQualifiers: {},
override: PrototypeUtil.isOverride(clazz),
conditionalOnMissing: PrototypeUtil.isConditionalOnMissing(clazz),
});
}

Expand Down
90 changes: 89 additions & 1 deletion tegg/core/metadata/src/model/graph/GlobalGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { debuglog } from 'node:util';

import { FrameworkErrorFormatter } from '@eggjs/errors';
import { Graph, GraphNode, type ModuleReference } from '@eggjs/tegg-common-util';
import { type InjectObjectDescriptor, type ProtoDescriptor, TeggScope, type TeggScopeBag } from '@eggjs/tegg-types';
import {
AccessLevel,
type InjectObjectDescriptor,
InitTypeQualifierAttribute,
LoadUnitNameQualifierAttribute,
type ProtoDescriptor,
TeggScope,
type TeggScopeBag,
} from '@eggjs/tegg-types';

import { EggPrototypeNotFound } from '../../errors.ts';
import type { ModuleDescriptor } from '../ModuleDescriptor.ts';
Expand Down Expand Up @@ -113,12 +121,92 @@ export class GlobalGraph {
}
}

/**
* The set of protos that genuinely compete for the same injection: same name
* + same init type + same USER qualifiers, and — because every proto carries
* an auto-added `LoadUnitName` (module) qualifier — a scope that is global for
* PUBLIC protos (they override across modules) but module-local for PRIVATE
* ones (a private proto is only visible in its own module, so two private
* same-name protos in different modules never compete). The auto-added
* `LoadUnitName`/`InitType` qualifiers are excluded from the user-qualifier
* signature so cross-module PUBLIC protos still group together.
*/
#protoCompeteKey(proto: ProtoDescriptor): string {
const userQualifiers = proto.qualifiers
.filter((q) => q.attribute !== InitTypeQualifierAttribute && q.attribute !== LoadUnitNameQualifierAttribute)
.map((q) => `${String(q.attribute)}=${String(q.value)}`)
.sort()
.join(',');
const scope = proto.accessLevel === AccessLevel.PUBLIC ? 'public' : `module:${proto.instanceModuleName}`;
return `${scope}|${String(proto.name)}|${proto.initType}|${userQualifiers}`;
}

/**
* Resolve override precedence before any edge is built or any proto is
* instantiated. Within each competing group (see {@link #protoCompeteKey})
* that carries an `@Override` or `@ConditionalOnMissing` marker, keep the
* highest tier — `@Override` > plain > `@ConditionalOnMissing` — and prune the
* losers from both the proto graph and their module's proto list, so they
* never get an inject edge nor an instantiation slot. Ties within the winning
* tier are left to normal resolution (qualifier disambiguation /
* MultiPrototypeFound).
*/
#pruneOverriddenProtos(): void {
type Node = GraphNode<ProtoNode, ProtoDependencyMeta>;
const byKey = new Map<string, Node[]>();
for (const node of this.protoGraph.nodes.values()) {
const key = this.#protoCompeteKey(node.val.proto);
let list = byKey.get(key);
if (!list) {
list = [];
byKey.set(key, list);
}
list.push(node);
}

const loserIds = new Set<string>();
for (const nodes of byKey.values()) {
if (nodes.length < 2) {
continue;
}
const hasMarker = nodes.some((n) => n.val.proto.override || n.val.proto.conditionalOnMissing);
if (!hasMarker) {
continue;
}
const overrides = nodes.filter((n) => n.val.proto.override);
const plains = nodes.filter((n) => !n.val.proto.override && !n.val.proto.conditionalOnMissing);
const winners = overrides.length > 0 ? overrides : plains.length > 0 ? plains : nodes;
for (const node of nodes) {
if (!winners.includes(node)) {
loserIds.add(node.id);
}
}
}

if (loserIds.size === 0) {
return;
}
for (const id of loserIds) {
this.protoGraph.removeVertex(id);
}
for (const moduleNode of this.moduleGraph.nodes.values()) {
const protos = moduleNode.val.protos;
for (let i = protos.length - 1; i >= 0; i--) {
if (loserIds.has(protos[i].id)) {
protos.splice(i, 1);
}
}
}
this.#protoNameIndex = null;
}

build(): void {
if (this.#buildState !== 'created') {
throw new Error(`global graph can only be built once (state: ${this.#buildState})`);
}
this.#buildState = 'building';
try {
this.#pruneOverriddenProtos();
for (const moduleNode of this.moduleGraph.nodes.values()) {
for (const protoNode of moduleNode.val.protos) {
for (const injectObj of protoNode.val.proto.injectObjects) {
Expand Down
Loading
Loading