Skip to content

Commit

Permalink
feat: use SingletonProto for egg ctx object (#92)
Browse files Browse the repository at this point in the history
* feat: use SingletonProto for egg ctx object
  • Loading branch information
killagu authored Feb 9, 2023
1 parent e14bdb2 commit 3385d57
Show file tree
Hide file tree
Showing 19 changed files with 213 additions and 105 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,31 @@ export class HelloService {
}
```

### egg 内 ctx/app 命名冲突

egg 内可能出现 ctx 和 app 上有同名对象的存在,我们可以通过使用 `EggQualifier` 来明确指定注入的对象来自 ctx 还是 app。不指定时,默认注入 app 上的对象。

###### 定义

```typescript
@EggQualifier(eggType: EggType)
```

###### 示例

```typescript
import { EggLogger } from 'egg';
import { Inject, EggQualifier, EggType } from '@eggjs/tegg';

@ContextProto()
export class HelloService {
@Inject()
// 明确指定注入 ctx 上的 foo 而不是 app 上的 foo
@EggQualifier(EggType.CONTEXT)
foo: Foo;
}
```

### 单测

#### 单测 Context
Expand Down
2 changes: 2 additions & 0 deletions core/core-decorator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export * from './src/decorator/InitTypeQualifier';
export * from './src/decorator/ModuleQualifier';
export * from './src/decorator/ContextProto';
export * from './src/decorator/SingletonProto';
export * from './src/decorator/EggQualifier';

export * from './src/enum/AccessLevel';
export * from './src/enum/ObjectInitType';
export * from './src/enum/EggType';

export * from './src/model/EggPrototypeInfo';
export * from './src/model/InjectObjectInfo';
Expand Down
11 changes: 11 additions & 0 deletions core/core-decorator/src/decorator/EggQualifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { QualifierUtil } from '../util/QualifierUtil';
import { EggProtoImplClass } from '../model/EggPrototypeInfo';
import { EggType } from '../enum/EggType';

export const EggQualifierAttribute = Symbol.for('Qualifier.Egg');

export function EggQualifier(eggType: EggType) {
return function(target: any, propertyKey: PropertyKey) {
QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, propertyKey, EggQualifierAttribute, eggType);
};
}
4 changes: 4 additions & 0 deletions core/core-decorator/src/enum/EggType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum EggType {
APP = 'APP',
CONTEXT = 'CONTEXT',
}
19 changes: 2 additions & 17 deletions core/eventbus-runtime/src/SingletonEventBus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AccessLevel, InitTypeQualifier, Inject, ObjectInitType, SingletonProto } from '@eggjs/core-decorator';
import { AccessLevel, Inject, SingletonProto } from '@eggjs/core-decorator';
import { EventBus, Events, EventWaiter, EventName, CORK_ID } from '@eggjs/eventbus-decorator';
import { ContextHandler, EggContext } from '@eggjs/tegg-runtime';
import type { EggLogger } from 'egg';
Expand Down Expand Up @@ -42,27 +42,12 @@ export class SingletonEventBus implements EventBus, EventWaiter {
@Inject({
name: 'logger',
})
@InitTypeQualifier(ObjectInitType.CONTEXT)
private readonly ctxLogger: EggLogger;

@Inject({
name: 'logger',
})
@InitTypeQualifier(ObjectInitType.SINGLETON)
private readonly singletonLogger: EggLogger;
private readonly logger: EggLogger;

private corkIdSequence = 0;

private readonly corkedEvents = new Map<string /* corkId */, CorkEvents>();

get logger(): EggLogger {
try {
return this.ctxLogger;
} catch (_) {
return this.singletonLogger;
}
}

/**
* only use for ensure event will happen
*/
Expand Down
5 changes: 4 additions & 1 deletion plugin/eventbus/lib/EventbusLoadUnitHook.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LifecycleHook } from '@eggjs/tegg';
import { EggQualifierAttribute, EggType, LifecycleHook, QualifierUtil } from '@eggjs/tegg';
import {
EggLoadUnitType,
EggPrototypeCreatorFactory,
Expand All @@ -14,6 +14,9 @@ const REGISTER_CLAZZ = [
SingletonEventBus,
];

// EggQualifier only for egg plugin
QualifierUtil.addProperQualifier(SingletonEventBus, 'logger', EggQualifierAttribute, EggType.APP);

export class EventbusLoadUnitHook implements LifecycleHook<LoadUnitLifecycleContext, LoadUnit> {
async postCreate(_ctx: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise<void> {
if (loadUnit.type === EggLoadUnitType.APP) {
Expand Down
7 changes: 7 additions & 0 deletions plugin/tegg/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { CompatibleUtil } from './lib/CompatibleUtil';
import { ModuleHandler } from './lib/ModuleHandler';
import { EggContextHandler } from './lib/EggContextHandler';
import { hijackRunInBackground } from './lib/run_in_background';
import { EggQualifierProtoHook } from './lib/EggQualifierProtoHook';

export default class App {
private readonly app: Application;
private compatibleHook?: EggContextCompatibleHook;
private eggContextHandler: EggContextHandler;
private eggQualifierProtoHook: EggQualifierProtoHook;

constructor(app: Application) {
this.app = app;
Expand All @@ -24,7 +26,9 @@ export default class App {
configDidLoad() {
this.eggContextHandler = new EggContextHandler(this.app);
this.app.eggContextHandler = this.eggContextHandler;
this.eggQualifierProtoHook = new EggQualifierProtoHook(this.app);
this.eggContextHandler.register();
this.app.loadUnitLifecycleUtil.registerLifecycle(this.eggQualifierProtoHook);
this.app.moduleHandler = new ModuleHandler(this.app);
}

Expand All @@ -41,5 +45,8 @@ export default class App {
if (this.compatibleHook) {
this.app.eggContextLifecycleUtil.deleteLifecycle(this.compatibleHook);
}
if (this.eggQualifierProtoHook) {
this.app.loadUnitLifecycleUtil.deleteLifecycle(this.eggQualifierProtoHook);
}
}
}
90 changes: 37 additions & 53 deletions plugin/tegg/lib/EggAppLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Application } from 'egg';
import { Loader, TeggError } from '@eggjs/tegg-metadata';
import {
AccessLevel,
EggProtoImplClass, InitTypeQualifierAttribute, LoadUnitNameQualifierAttribute,
EggProtoImplClass,
EggQualifierAttribute,
EggType,
InitTypeQualifierAttribute,
LoadUnitNameQualifierAttribute,
ObjectInitType,
PrototypeUtil,
QualifierUtil,
Expand All @@ -12,9 +16,9 @@ import { COMPATIBLE_PROTO_IMPLE_TYPE } from './EggCompatibleProtoImpl';
import { BackgroundTaskHelper } from '@eggjs/tegg-background-task';
import { EggObjectFactory } from '@eggjs/tegg-dynamic-inject-runtime';

const APP_CLAZZ_BLACK_LIST = [ 'eggObjectFactory' ];
const DEFAULT_APP_CLAZZ = [];
const DEFAULT_CONTEXT_CLAZZ = [
export const APP_CLAZZ_BLACK_LIST = [ 'eggObjectFactory' ];
export const DEFAULT_APP_CLAZZ: string[] = [];
export const DEFAULT_CONTEXT_CLAZZ = [
'user',
];

Expand All @@ -25,46 +29,41 @@ export class EggAppLoader implements Loader {
this.app = app;
}

private buildAppClazz(name: string): EggProtoImplClass {
private buildClazz(name: string, eggType: EggType): EggProtoImplClass {
const app = this.app;
const func: EggProtoImplClass = function() {
return app[name];
} as any;
PrototypeUtil.setIsEggPrototype(func);
PrototypeUtil.setFilePath(func, 'mock_file_path');
PrototypeUtil.setProperty(func, {
name,
initType: ObjectInitType.SINGLETON,
accessLevel: AccessLevel.PUBLIC,
protoImplType: COMPATIBLE_PROTO_IMPLE_TYPE,
});
QualifierUtil.addProtoQualifier(func, LoadUnitNameQualifierAttribute, 'app');
QualifierUtil.addProtoQualifier(func, InitTypeQualifierAttribute, ObjectInitType.SINGLETON);
return func;
}

private buildCtxClazz(name: string): EggProtoImplClass {
const temp = {
[name]: function(ctx) {
let func: EggProtoImplClass;
if (eggType === EggType.APP) {
func = function() {
return app[name];
} as any;
} else {
func = function() {
const ctx = app.currentContext;
if (!ctx) {
// ctx has been destroyed, throw humanize error info
throw TeggError.create(`Can not read property \`${name}\` because egg ctx has been destroyed`, 'read_after_ctx_destroyed');
}

return ctx[name];
} as any,
};
const func = temp[name];
} as any;
}
Object.defineProperty(func, 'name', {
value: name,
writable: false,
enumerable: false,
configurable: true,
});
PrototypeUtil.setIsEggPrototype(func);
PrototypeUtil.setFilePath(func, 'mock_file_path');
PrototypeUtil.setProperty(func, {
name,
initType: ObjectInitType.CONTEXT,
initType: ObjectInitType.SINGLETON,
accessLevel: AccessLevel.PUBLIC,
protoImplType: COMPATIBLE_PROTO_IMPLE_TYPE,
});
QualifierUtil.addProtoQualifier(func, LoadUnitNameQualifierAttribute, 'app');
QualifierUtil.addProtoQualifier(func, InitTypeQualifierAttribute, ObjectInitType.CONTEXT);
QualifierUtil.addProtoQualifier(func, InitTypeQualifierAttribute, ObjectInitType.SINGLETON);
QualifierUtil.addProtoQualifier(func, EggQualifierAttribute, eggType);
return func;
}

Expand All @@ -73,6 +72,12 @@ export class EggAppLoader implements Loader {
const func: EggProtoImplClass = function() {
return app.getLogger(name);
} as any;
Object.defineProperty(func, 'name', {
value: name,
writable: false,
enumerable: false,
configurable: true,
});
PrototypeUtil.setIsEggPrototype(func);
PrototypeUtil.setFilePath(func, 'mock_file_path');
PrototypeUtil.setProperty(func, {
Expand All @@ -83,26 +88,7 @@ export class EggAppLoader implements Loader {
});
QualifierUtil.addProtoQualifier(func, LoadUnitNameQualifierAttribute, 'app');
QualifierUtil.addProtoQualifier(func, InitTypeQualifierAttribute, ObjectInitType.SINGLETON);
return func;
}

private buildCtxLoggerClazz(name: string): EggProtoImplClass {
const temp = {
[name]: function(ctx) {
return ctx.getLogger(name);
} as any,
};
const func = temp[name];
PrototypeUtil.setIsEggPrototype(func);
PrototypeUtil.setFilePath(func, 'mock_file_path');
PrototypeUtil.setProperty(func, {
name,
initType: ObjectInitType.CONTEXT,
accessLevel: AccessLevel.PUBLIC,
protoImplType: COMPATIBLE_PROTO_IMPLE_TYPE,
});
QualifierUtil.addProtoQualifier(func, LoadUnitNameQualifierAttribute, 'app');
QualifierUtil.addProtoQualifier(func, InitTypeQualifierAttribute, ObjectInitType.CONTEXT);
QualifierUtil.addProtoQualifier(func, EggQualifierAttribute, EggType.APP);
return func;
}

Expand All @@ -128,16 +114,14 @@ export class EggAppLoader implements Loader {
...DEFAULT_CONTEXT_CLAZZ,
]));
const loggerNames = this.getLoggerNames(allContextClazzNames);
const allSingletonClazzs = allSingletonClazzNames.map(name => this.buildAppClazz(name));
const allContextClazzs = allContextClazzNames.map(name => this.buildCtxClazz(name));
const allSingletonClazzs = allSingletonClazzNames.map(name => this.buildClazz(name, EggType.APP));
const allContextClazzs = allContextClazzNames.map(name => this.buildClazz(name, EggType.CONTEXT));
const appLoggerClazzs = loggerNames.map(name => this.buildAppLoggerClazz(name));
const ctxLoggerClazzs = loggerNames.map(name => this.buildCtxLoggerClazz(name));

return [
...allSingletonClazzs,
...allContextClazzs,
...appLoggerClazzs,
...ctxLoggerClazzs,

// inner helper class list
// TODO: should auto the inner class
Expand Down
6 changes: 2 additions & 4 deletions plugin/tegg/lib/EggCompatibleObject.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { EggCompatibleProtoImpl } from './EggCompatibleProtoImpl';
import {
ContextHandler,
EggObject,
EggObjectFactory,
} from '@eggjs/tegg-runtime';
Expand All @@ -19,16 +18,15 @@ export class EggCompatibleObject implements EggObject {
constructor(name: EggObjectName, proto: EggCompatibleProtoImpl) {
this.proto = proto;
this.name = name;
const ctx = ContextHandler.getContext();
this.id = IdenticalUtil.createObjectId(this.proto.id, ctx?.id);
this.id = IdenticalUtil.createObjectId(this.proto.id);
}

// If the egg object is a getter,
// access may have side effect.
// So access egg object lazy.
get obj() {
if (!this[OBJ]) {
this[OBJ] = this.proto.constructorEggCompatibleObject();
this[OBJ] = this.proto.constructEggObject();
}
return this[OBJ];
}
Expand Down
10 changes: 1 addition & 9 deletions plugin/tegg/lib/EggCompatibleProtoImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import {
Id,
IdenticalUtil,
} from '@eggjs/tegg';
import { ContextHandler } from '@eggjs/tegg-runtime';
import {
EggPrototype,
InjectObjectProto,
EggPrototypeLifecycleContext,
} from '@eggjs/tegg-metadata';
import { EGG_CONTEXT } from '@eggjs/egg-module-common';


export const COMPATIBLE_PROTO_IMPLE_TYPE = 'EGG_COMPATIBLE';
Expand Down Expand Up @@ -65,13 +63,7 @@ export class EggCompatibleProtoImpl implements EggPrototype {
}

constructEggObject(): object {
return {};
}

constructorEggCompatibleObject() {
const teggContext = ContextHandler.getContext();
const ctx = teggContext?.get(EGG_CONTEXT);
return Reflect.apply(this.clazz, null, [ ctx ]);
return Reflect.apply(this.clazz, null, []);
}

getMetaData<T>(metadataKey: MetaDataKey): T | undefined {
Expand Down
6 changes: 0 additions & 6 deletions plugin/tegg/lib/EggContextCompatibleHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { EggContainerFactory, EggContext, EggContextLifecycleContext } from '@eg
import { EggPrototype } from '@eggjs/tegg-metadata';
import { ModuleHandler } from './ModuleHandler';
import { ROOT_PROTO } from '@eggjs/egg-module-common';
import { EggCompatibleProtoImpl } from './EggCompatibleProtoImpl';

export class EggContextCompatibleHook implements LifecycleHook<EggContextLifecycleContext, EggContext> {
private readonly moduleHandler: ModuleHandler;
Expand All @@ -15,11 +14,6 @@ export class EggContextCompatibleHook implements LifecycleHook<EggContextLifecyc
for (const loadUnitInstance of this.moduleHandler.loadUnitInstances) {
const iterator = loadUnitInstance.loadUnit.iterateEggPrototype();
for (const proto of iterator) {
// skip the egg compatible object
// If the egg compatible object has beed used with inject,
// it will be refer by inject info. And it can not be used
// with ctx.module.${moduleName} so skip it is safe.
if (proto instanceof EggCompatibleProtoImpl) continue;
if (proto.initType === ObjectInitType.CONTEXT) {
this.requestProtoList.push(proto);
} else if (proto.initType === ObjectInitType.SINGLETON) {
Expand Down
Loading

0 comments on commit 3385d57

Please sign in to comment.