Skip to content

Commit

Permalink
fix: union model in promise
Browse files Browse the repository at this point in the history
  • Loading branch information
bytemain committed Sep 7, 2023
1 parent ede8dc8 commit 700cad3
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 107 deletions.
2 changes: 1 addition & 1 deletion src/declare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export type MethodName = string | number | symbol;
export enum HookType {
Before = 'Before',
After = 'After',
Around = 'Around ',
Around = 'Around',
AfterReturning = 'AfterReturning',
AfterThrowing = 'AfterThrowing',
}
Expand Down
66 changes: 66 additions & 0 deletions src/helper/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
interface CallStack {
index: number;
}

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

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

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;
awaitPromise?: boolean;
}

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

Check warning on line 29 in src/helper/compose.ts

View check run for this annotation

Codecov / codecov/patch

src/helper/compose.ts#L29

Added line #L29 was not covered by tests
}

stack.index = depth;

const { length } = middlewareList;

let maybePromise: Promise<void> | void;
if (depth <= length) {
if (depth < length) {
const middleware = middlewareList[depth];

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

if (middleware.awaitPromise) {
maybePromise = Promise.resolve(maybePromise);
}
} else if (ctx.proceed) {
// 这里可以不用 Promise.resolve
// 但是为了兼容旧的表现,这里还是加上了
maybePromise = ctx.proceed();
}
}

return maybePromise;
}

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

export const HOOKED_SYMBOL = Symbol('COMMON_DI_HOOKED');

Expand Down Expand Up @@ -93,14 +94,14 @@ export function createHookedFunction<ThisType, Args extends any[], Result>(
});

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,7 +124,7 @@ export function createHookedFunction<ThisType, Args extends any[], Result>(
let p: Promise<Result> | undefined;
if (promise) {
p = promise;
} else if (isPromiseLike(ret)) {
} else if (ret && isPromiseLike(ret)) {
p = ret;
}

Expand All @@ -144,8 +145,16 @@ export function createHookedFunction<ThisType, Args extends any[], Result>(
}
}

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

const aroundJoinPoint: IAroundJoinPoint<ThisType, Args, Result> = {
getArgs: () => {
return args;
Expand All @@ -162,35 +171,21 @@ export function createHookedFunction<ThisType, Args extends any[], Result>(
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
140 changes: 140 additions & 0 deletions test/aspect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Injectable, Aspect, Around, IAroundJoinPoint, Injector } from '../src';

describe('aspect', () => {
jest.setTimeout(1000 * 1000);
/**
* 下面的 case 目前输出:
* TestAspect2 async 10
* 然后执行超时
*/
it('异步的hook异常, promise无法结束', 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);
});

/**
* 下面的 case 目前输出:
* TestAspect2 async 10
* TestAspect2 undefined
* TestClass invoke result undefined
* 到这里单测就停了,其实后续会再执行
* TestAspect undefined
* TestClass add result 3
*/
it('异步的hook异常, 等待的promise错误', 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);
});
});
82 changes: 82 additions & 0 deletions test/helper/compose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import compose, { Middleware } from '../../src/helper/compose';

describe('di compose', () => {
it('can worked', async () => {
interface ExampleContext {
getName(): string;
getResult(): any;
}

const middleware1: Middleware<ExampleContext> = async (ctx) => {
const name = ctx.getName();
console.log(`middleware1: ${name}`);
await ctx.proceed();
const result = ctx.getResult();
console.log(`middleware1 result: ${result}`);
console.log(`middleware1 after: ${name}`);
};

const middleware2: Middleware<ExampleContext> = async (ctx) => {
const name = ctx.getName();
console.log(`middleware2: ${name}`);
await ctx.proceed();
const result = ctx.getResult();
console.log(`middleware2 result: ${result}`);
console.log(`middleware2 after: ${name}`);
};

const all = compose<ExampleContext>([middleware1, middleware2]);
let ret = undefined as any;
all({
getName() {
return 'example';
},
async proceed() {
console.log('invoked');
ret = 'final result';
},
getResult(): any {
return ret;
},
});
});

it('can worked with sync', async () => {
interface ExampleContext {
getName(): string;
getResult(): any;
}
const middleware1: Middleware<ExampleContext> = async (ctx) => {
const name = ctx.getName();
console.log(`middleware1: ${name}`);
ctx.proceed();
const result = ctx.getResult();
console.log(`middleware1 result: ${result}`);
console.log(`middleware1 after: ${name}`);
};

const middleware2: Middleware<ExampleContext> = async (ctx) => {
const name = ctx.getName();
console.log(`middleware2: ${name}`);
ctx.proceed();
const result = ctx.getResult();
console.log(`middleware2 result: ${result}`);
console.log(`middleware2 after: ${name}`);
};

const all = compose<ExampleContext>([middleware1, middleware2]);
let ret = undefined as any;
all({
getName() {
return 'example';
},
proceed() {
console.log('invoked');
ret = 'final result';
},
getResult(): any {
return ret;
},
});
});
});
Loading

0 comments on commit 700cad3

Please sign in to comment.