Skip to content

Commit

Permalink
feat: add async interceptor (#3894)
Browse files Browse the repository at this point in the history
  • Loading branch information
ybzky authored Oct 30, 2024
1 parent 7f1cf91 commit f007660
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 2 deletions.
64 changes: 64 additions & 0 deletions packages/core/src/common/__tests__/interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright 2023-present DreamNum Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, expect, it } from 'vitest';
import { AsyncInterceptorManager, createAsyncInterceptorKey } from '../interceptor';

describe('Test interceptor', () => {
it('should return "true" when each interceptor returns true ', async () => {
const INTERCEPTOR_KEY = createAsyncInterceptorKey<boolean, boolean>('test');
const interceptor = new AsyncInterceptorManager({ INTERCEPTOR_KEY });

interceptor.interceptAsync(interceptor.getInterceptPoints().INTERCEPTOR_KEY, {
priority: 0,
handler: async (value, context, next) => {
return await next(value);
},
});

interceptor.interceptAsync(interceptor.getInterceptPoints().INTERCEPTOR_KEY, {
priority: 10,
handler: async (value, context, next) => {
return await next(true);
},
});

const interceptorRes = await interceptor.fetchThroughAsyncInterceptors(INTERCEPTOR_KEY)(false, true);
expect(interceptorRes).toBe(true);
});

it('should return "false" when one of the interceptor returns false', async () => {
const INTERCEPTOR_KEY = createAsyncInterceptorKey<boolean, boolean>('test');
const interceptor = new AsyncInterceptorManager({ INTERCEPTOR_KEY });

interceptor.interceptAsync(interceptor.getInterceptPoints().INTERCEPTOR_KEY, {
priority: 0,
handler: async (value, context, next) => {
return await next(value);
},
});

interceptor.interceptAsync(interceptor.getInterceptPoints().INTERCEPTOR_KEY, {
priority: 10,
handler: async (value, context, next) => {
return await next(false);
},
});

const interceptorRes = await interceptor.fetchThroughAsyncInterceptors(INTERCEPTOR_KEY)(false, true);
expect(interceptorRes).toBe(false);
});
});
95 changes: 94 additions & 1 deletion packages/core/src/common/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import { remove } from './array';
import type { Nullable } from '../shared/types';
import { remove } from './array';

export type InterceptorHandler<M = unknown, C = unknown> = (
value: Nullable<M>,
Expand Down Expand Up @@ -118,3 +118,96 @@ export class InterceptorManager<P extends Record<string, IInterceptor<any, any>>
this._interceptorsByName.clear();
}
}

export function createAsyncInterceptorKey<T, C>(key: string): IAsyncInterceptor<T, C> {
const symbol = `sheet_async_interceptor_${key}`;
return symbol as unknown as IAsyncInterceptor<T, C>;
};

export type AsyncInterceptorHandler<M = unknown, C = unknown> = (
value: Nullable<M>,
context: C,
next: (value: Nullable<M>) => Promise<Nullable<M>>
) => Promise<Nullable<M>>;

export interface IAsyncInterceptor<M, C> {
priority?: number;
handler: AsyncInterceptorHandler<M, C>;
}

export type IComposeAsyncInterceptors<T = any, C = any> = (
interceptors: Array<IAsyncInterceptor<T, C>>
) => (initValue: Nullable<T>, initContext: C) => Promise<Nullable<T>>;

export const composeAsyncInterceptors = <T, C>(
interceptors: Array<IAsyncInterceptor<T, C>>
): ((initialValue: Nullable<T>, context: C) => Promise<Nullable<T>>) => {
return async function (initialValue: Nullable<T>, context: C) {
let index = -1;
let value: Nullable<T> = initialValue;

for (let i = 0; i <= interceptors.length; i++) {
if (i <= index) {
throw new Error('[SheetInterceptorService]: next() called multiple times!');
}

index = i;

if (i === interceptors.length) {
return value;
}

const interceptor = interceptors[i];
let nextCalled = false;

value = await interceptor.handler!(value, context, async (nextValue) => {
nextCalled = true;
return nextValue;
});

if (!nextCalled) {
break;
}
}

return value;
};
};

export class AsyncInterceptorManager<P extends Record<string, IAsyncInterceptor<any, any>>> {
private _asyncInterceptorsByName: Map<string, Array<IAsyncInterceptor<unknown, unknown>>> = new Map();
private _asyncInterceptorPoints: P;

constructor(asyncInterceptorPoints: P) {
this._asyncInterceptorPoints = asyncInterceptorPoints;
}

public fetchThroughAsyncInterceptors<T, C>(name: IAsyncInterceptor<T, C>) {
const key = name as unknown as string;
const interceptors = this._asyncInterceptorsByName.get(key) as unknown as Array<typeof name>;
return composeAsyncInterceptors(interceptors || []);
}

public async interceptAsync<T extends IAsyncInterceptor<any, any>>(name: T, interceptor: T) {
const key = name as unknown as string;
if (!this._asyncInterceptorsByName.has(key)) {
this._asyncInterceptorsByName.set(key, []);
}
const interceptors = this._asyncInterceptorsByName.get(key)!;
interceptors.push(interceptor);

this._asyncInterceptorsByName.set(
key,
interceptors.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)) // from large to small
);
return () => remove(this._asyncInterceptorsByName.get(key)!, interceptor);
}

public getInterceptPoints() {
return this._asyncInterceptorPoints;
}

public dispose() {
this._asyncInterceptorsByName.clear();
}
}
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export { shallowEqual } from './common/equal';
export { CustomCommandExecutionError } from './common/error';
export { throttle } from './common/function';
export type { ICellInterceptor, IComposeInterceptors, IInterceptor, InterceptorHandler } from './common/interceptor';
export { composeInterceptors, createInterceptorKey, InterceptorEffectEnum, InterceptorManager } from './common/interceptor';
export { AsyncInterceptorManager, composeInterceptors, createAsyncInterceptorKey, createInterceptorKey, InterceptorEffectEnum, InterceptorManager } from './common/interceptor';
export type { Serializable } from './common/json';
export { MemoryCursor } from './common/memory-cursor';
export { mixinClass } from './common/mixin';
Expand Down

0 comments on commit f007660

Please sign in to comment.