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

fix: union model in promise #107

Merged
merged 11 commits into from
Sep 26, 2023
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
11 changes: 10 additions & 1 deletion src/declare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
export enum HookType {
Before = 'Before',
After = 'After',
Around = 'Around ',
Around = 'Around',
AfterReturning = 'AfterReturning',
AfterThrowing = 'AfterThrowing',
}
Expand Down Expand Up @@ -210,7 +210,7 @@
hook: IAfterThrowingAspectHookFunction<ThisType, Args, Result>;
}

export interface IJoinPoint<ThisType = any, Args extends any[] = any, Result = any> {

Check warning on line 213 in src/declare.ts

View workflow job for this annotation

GitHub Actions / ci

'Result' is defined but never used
getThis(): ThisType;
getMethodName(): MethodName;
getOriginalArgs(): Args;
Expand Down Expand Up @@ -240,7 +240,7 @@
getResult(): Result;
}

export interface IAfterThrowingJoinPoint<ThisType, Args extends any[], Result> extends IJoinPoint<ThisType, Args> {

Check warning on line 243 in src/declare.ts

View workflow job for this annotation

GitHub Actions / ci

'Result' is defined but never used
getError(): Error | undefined;
}

Expand All @@ -259,3 +259,12 @@
// Whether to wait for the hook (if the return value of the hook is a promise)
await?: boolean;
}

export interface IAroundHookOptions {
/**
* @deprecated AroundHook will always await the promise, it act as the union model.
*
* Whether to wait for the hook (if the return value of the hook is a promise)
*/
await?: boolean;
}
3 changes: 2 additions & 1 deletion src/decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
IHookOptions,
IAfterReturningAspectHookFunction,
IAfterThrowingAspectHookFunction,
IAroundHookOptions,
} from './declare';
import { markAsAspect, markAsHook } from './helper';

Expand Down Expand Up @@ -239,7 +240,7 @@ export function After<ThisType = any, Args extends any[] = any, Result = any>(
export function Around<ThisType = any, Args extends any[] = any, Result = any>(
token: Token,
method: MethodName,
options: IHookOptions = {},
options: IAroundHookOptions = {},
) {
return <T extends Record<K, IAroundAspectHookFunction<ThisType, Args, Result>>, K extends MethodName>(
target: T,
Expand Down
54 changes: 54 additions & 0 deletions src/helper/compose.ts
bytemain marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
interface CallStack {
depth: number;
}

export type Optional<T extends object, K extends keyof T = keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

export type Context<T> = {
proceed(): Promise<void> | void;
} & T;

export type PureContext<T> = Optional<Context<T>, 'proceed'>;

export interface Composed<C> {
(ctx: PureContext<C>): Promise<void> | void;
}

export interface Middleware<C> {
(ctx: Context<C>): Promise<void> | void;
}

function dispatch<C>(
middlewareList: Middleware<C>[],
idx: number,
stack: CallStack,
ctx: PureContext<C>,
): Promise<void> | void {
if (idx <= stack.depth) {
throw new Error('ctx.proceed() called multiple times');
}

stack.depth = idx;

let maybePromise: Promise<void> | void;

if (idx < middlewareList.length) {
const middleware = middlewareList[idx];

maybePromise = middleware({
...ctx,
proceed: () => dispatch(middlewareList, idx + 1, stack, ctx),
} as Context<C>);
} else if (ctx.proceed) {
maybePromise = ctx.proceed();
}

return maybePromise;
}

export default function compose<C>(middlewareList: Middleware<C>[]): Composed<C> {
return (ctx) => {
const stack: CallStack = { depth: -1 };
return dispatch<C>(middlewareList, 0, stack, ctx);
};
}
51 changes: 20 additions & 31 deletions src/helper/hook-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
IAfterThrowingJoinPoint,
IInstanceHooks,
} from '../declare';
import compose, { Middleware } from './compose';

export const HOOKED_SYMBOL = Symbol('COMMON_DI_HOOKED');

Expand Down Expand Up @@ -93,14 +94,14 @@
});

return function (this: any, ...args: Args) {
let promise: Promise<any> | undefined;
let promise: Promise<any> | undefined | void;
let ret: Result = undefined as any;
let error: Error | undefined;
const self = this;
const originalArgs: Args = args;

try {
runAroundHooks();
promise = runAroundHooks();

if (promise) {
// If there is one hook that is asynchronous, convert all of them to asynchronous.
Expand All @@ -123,8 +124,6 @@
let p: Promise<Result> | undefined;
if (promise) {
p = promise;
} else if (isPromiseLike(ret)) {
p = ret;
}

if (p) {
Expand All @@ -144,8 +143,13 @@
}
}

function runAroundHooks(): Promise<void> | undefined {
let i = 0;
function runAroundHooks(): Promise<void> | void {
const hooks = aroundHooks.map((v) => {
const fn = v.hook as Middleware<IAroundJoinPoint<ThisType, Args, Result>>;
return fn;
});
const composed = compose<IAroundJoinPoint<ThisType, Args, Result>>(hooks);

const aroundJoinPoint: IAroundJoinPoint<ThisType, Args, Result> = {
getArgs: () => {
return args;
Expand All @@ -162,35 +166,21 @@
getThis: () => {
return self;
},
proceed: () => {
i++;
promise = runAroundHookAtIndex(i);
return promise;
},
setArgs: (_args: Args) => {
args = _args;
},
setResult: (_ret: Result) => {
ret = _ret;
},
};

function runAroundHookAtIndex(index: number): Promise<void> | undefined {
const aroundHook = aroundHooks[index];
if (!aroundHook) {
// the innermost layer
wrapped();
if (promise) {
// it seems that asynchronous operations are being performed within the "before" and "after" hooks.
return promise;
proceed: () => {
const maybePromise = wrapped();
if (maybePromise && isPromiseLike(maybePromise)) {
return maybePromise.then(() => Promise.resolve());
}
} else {
promise = runOneHook(aroundHook, aroundJoinPoint, promise);
return promise;
}
}
},
};

return runAroundHookAtIndex(0);
return composed(aroundJoinPoint);
}

function runBeforeHooks(): Promise<void> | undefined {
Expand Down Expand Up @@ -317,7 +307,7 @@
});
}

function wrapped(): Result {
function wrapped(): Result | void | Promise<Result | void> {
promise = runBeforeHooks();

if (promise) {
Expand All @@ -340,9 +330,9 @@
if (promise) {
return promise.then(() => {
return ret;
}) as any; // 挂载了async hook方法
});
} else {
return ret as any;
return ret;
}
}
};
Expand Down Expand Up @@ -403,7 +393,6 @@
P extends IJoinPoint,
>(hook: T, joinPoint: P, promise: Promise<any> | undefined): Promise<void> | undefined {
if (hook.awaitPromise) {
// 如果要求await hook,如果之前有promise,直接用,不然创建Promise给下一个使用
promise = promise || Promise.resolve();
promise = promise.then(() => {
return hook.hook(joinPoint);
Expand Down Expand Up @@ -457,7 +446,7 @@
hooks = this.parent.getHooks(token, method);
}
if (this.hooks.get(token)?.has(method)) {
hooks = hooks.concat(this.hooks.get(token)!.get(method)!);

Check warning on line 449 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion

Check warning on line 449 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
}
return hooks;
}
Expand All @@ -467,12 +456,12 @@
if (!this.hooks.has(token)) {
this.hooks.set(token, new Map());
}
const instanceHooks = this.hooks.get(token)!;

Check warning on line 459 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
if (!instanceHooks.has(hook.method)) {
instanceHooks.set(hook.method, []);
}
// TODO: 支持order
instanceHooks.get(hook.method)!.push(hook);

Check warning on line 464 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
return {
dispose: () => {
this.removeOneHook(hook);
Expand All @@ -485,13 +474,13 @@
if (!this.hooks.has(token)) {
return;
}
const instanceHooks = this.hooks.get(token)!;

Check warning on line 477 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
if (!instanceHooks.has(hook.method)) {
return;
}
const index = instanceHooks.get(hook.method)!.indexOf(hook);

Check warning on line 481 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
if (index > -1) {
instanceHooks.get(hook.method)!.splice(index, 1);

Check warning on line 483 in src/helper/hook-helper.ts

View workflow job for this annotation

GitHub Actions / ci

Forbidden non-null assertion
}
}
}
Expand Down
127 changes: 127 additions & 0 deletions test/aspect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { Injectable, Aspect, Around, IAroundJoinPoint, Injector } from '../src';

describe('aspect', () => {
jest.setTimeout(50 * 1000);

it('test around hook: union model1', async () => {
function delay(value: number, time: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, time);
});
}

@Injectable()
class TestClass {
async add(a: number, b: number): Promise<number> {
const data = await delay(a + b, 1000);
console.log('TestClass add result', data);
return data;
}
}

@Aspect()
@Injectable()
class TestAspect {
@Around<TestClass, [number, number], number>(TestClass, 'add', { await: true })
async interceptAdd(joinPoint: IAroundJoinPoint<TestClass, [number, number], number>) {
expect(joinPoint.getMethodName()).toBe('add');
expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array);
expect(joinPoint.getThis()).toBeInstanceOf(TestClass);
await joinPoint.proceed();
const result = await joinPoint.getResult();
console.log('TestAspect', result);
}
}

@Aspect()
@Injectable()
class TestAspect2 {
@Around<TestClass, [number, number], number>(TestClass, 'add', { await: true })
async interceptAdd(joinPoint: IAroundJoinPoint<TestClass, [number, number], number>) {
const other = await delay(10, 1000);
console.log('TestAspect2 async', other);
expect(joinPoint.getMethodName()).toBe('add');
expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array);
expect(joinPoint.getThis()).toBeInstanceOf(TestClass);
await joinPoint.proceed();
const result = await joinPoint.getResult();
console.log('TestAspect2', result);
}
}

const injector = new Injector();
injector.addProviders(TestClass);
injector.addProviders(TestAspect);
injector.addProviders(TestAspect2);

const testClass = injector.get(TestClass);

const result = await testClass.add(1, 2);
console.log('TestClass invoke result', result);

expect(result).toBe(3);
});

it('test union model: union model2', async () => {
function delay(value: number, time: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, time);
});
}

@Injectable()
class TestClass {
async add(a: number, b: number): Promise<number> {
const data = await delay(a + b, 1000);
console.log('TestClass add result', data);
return data;
}
}

@Aspect()
@Injectable()
class TestAspect {
@Around<TestClass, [number, number], number>(TestClass, 'add', { await: true })
async interceptAdd(joinPoint: IAroundJoinPoint<TestClass, [number, number], number>) {
expect(joinPoint.getMethodName()).toBe('add');
expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array);
expect(joinPoint.getThis()).toBeInstanceOf(TestClass);
joinPoint.proceed();
const result = await joinPoint.getResult();
console.log('TestAspect', result);
}
}

@Aspect()
@Injectable()
class TestAspect2 {
@Around<TestClass, [number, number], number>(TestClass, 'add', { await: true })
async interceptAdd(joinPoint: IAroundJoinPoint<TestClass, [number, number], number>) {
const other = await delay(10, 1000);
console.log('TestAspect2 async', other);
expect(joinPoint.getMethodName()).toBe('add');
expect(joinPoint.getOriginalArgs()).toBeInstanceOf(Array);
expect(joinPoint.getThis()).toBeInstanceOf(TestClass);
joinPoint.proceed();
const result = await joinPoint.getResult();
console.log('TestAspect2', result);
}
}

const injector = new Injector();
injector.addProviders(TestClass);
injector.addProviders(TestAspect);
injector.addProviders(TestAspect2);

const testClass = injector.get(TestClass);

const result = await testClass.add(1, 2);
console.log('TestClass invoke result', result);

expect(result).toBe(3);
});
});
Loading
Loading