api-simplex-handler
Version:
一个api封装解析器
318 lines (281 loc) • 12.2 kB
text/typescript
import {IApiHandler} from "./IApiHandler";
import {IApiParamsCollection} from "./IApiParamsCollection";
import {IApi} from "./IApi";
import {
CacheData,
ExceptionFunction,
ExpectFunction,
FailFunction,
GeneralObject,
Hanger,
HangerIndex,
ObjectMapping,
ResolveParams,
ResponseHandlerDefineFunction,
ResponseHandlerFunction,
SendHandlerResponse,
SuccessFunction
} from "./TypeDefine";
import {ISendResponse} from "./ISendResponse";
abstract class ApiHandler<T extends IApi> implements IApiHandler<T['params']> {
api: T;
expect: ExpectFunction;
success: SuccessFunction;
exception: ExceptionFunction;
fail: FailFunction;
private cache: Map<String, CacheData> = new Map<String, CacheData>();
private argsQueue: Array<(value: any) => void> = new Array<(value: any) => void>();
private limitSet: number = 0;
private beforeList: Hanger<T>['before'][] = new Array<Hanger<T>['before']>()
private afterList: Hanger<T>['after'][] = new Array<Hanger<T>['after']>()
private guardList: Hanger<T>['guard'][] = new Array<Hanger<T>['guard']>()
private completeList: Hanger<T>['complete'][] = new Array<Hanger<T>['complete']>()
public getURL(): string {
if (!this.api.path) throw new Error("not setting url")
return this.api.path;
}
public composeURL(args: ResolveParams<T['params']>): string {
if (!this.api.path) throw new Error("not setting url")
let path = this.api.path;
if (Array.isArray(this.api.params)) {
const params = ApiHandler.assemblyParameters(this.api.params, args, this.api);
if (typeof params === 'string')
path += params
} else if (typeof this.api.params !== 'undefined') {
const params = ApiHandler.assemblyParametersToUnion(this.api.params, args, this.api);
path += params.path + params.query
}
return path;
}
protected constructor(api: T, responseHandlerDefineFunction: ResponseHandlerDefineFunction) {
this.api = api;
if (Array.isArray(this.api.hanger)) {
this.api.hanger.forEach((v) => this.analysisHanger(v, api));
} else if (typeof this.api.hanger !== 'undefined')
this.analysisHanger(this.api.hanger, api)
this.expect = responseHandlerDefineFunction.expectFunction;
this.success = responseHandlerDefineFunction.successFunction;
this.exception = responseHandlerDefineFunction.exceptionFunction;
this.fail = responseHandlerDefineFunction.failFunction;
}
private analysisHanger(hangerIndex: HangerIndex<T>, api: T) {
const {before, after, guard, complete} = hangerIndex(api)
if (typeof before !== "undefined")
this.beforeList.push(before)
if (typeof after !== "undefined")
this.afterList.push(after)
if (typeof guard !== "undefined")
this.guardList.push(guard)
if (typeof complete !== "undefined")
this.completeList.push(complete)
}
abstract send(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> ;
public clearCache() {
for (const [key, cacheData] of this.cache.entries()) {
localStorage.removeItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`)
}
this.cache.clear();
for (const key in localStorage) {
if (key.startsWith(`api-handler-${this.api.cacheKey ?? ''}-${this.api.path}`))
localStorage.removeItem(key);
}
}
private saveCacheHandler(args: ResolveParams<T['params']>, data: ISendResponse<any>): void {
if (typeof this.api.cachePolicy === "undefined" || this.api.cachePolicy === 'noCache') return;
const key = this.api.path + JSON.stringify(args)
if (this.cache.has(key)) {
(<CacheData>this.cache.get(key)).lastTime = new Date();
} else {
const cacheData: CacheData = {
data,
lastTime: new Date()
}
this.cache.set(key, cacheData);
if (this.api.cachePolicy === 'save')
localStorage.setItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`, JSON.stringify(data))
}
}
private async cacheHandler(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> {
const key = this.api.path + JSON.stringify(args)
if (!this.cache.has(key)) {
if (this.api.cachePolicy === 'save') {
const localStorageData = localStorage.getItem(`api-handler-${this.api.cacheKey ?? ''}-${key}`)
if (localStorageData === null) return this.send(args);
else return Promise.resolve().then(() => {
const cacheData = JSON.parse(localStorageData) as ISendResponse<any>;
this.cache.set(key, {
data: cacheData,
lastTime: new Date()
} as CacheData);
return cacheData;
})
}
return this.send(args);
}
const cacheData: CacheData = this.cache.get(key) as CacheData
if (this.api.cachePolicy === 'dynamic') {
if (new Date().getTime() - cacheData.lastTime.getTime() > (this.api.expirationTime ?? 1000 * 60 * 30))
return this.send(args);
}
return Promise.resolve().then(() => {
return cacheData.data;
})
}
public async sendNotSuccessTips(args: ResolveParams<T['params']>): Promise<SendHandlerResponse> {
return this.sendHandler(args, {successFunction: null})
}
private startNext() {
if (this.argsQueue.length == 0) return;
if (typeof this.api.limit === 'undefined' || this.api.limit <= 0) return;
while (this.limitSet < this.api.limit) {
this.limitSet += 1;
const resolve = this.argsQueue.shift() as ((value: any) => void);
resolve(1);
}
}
private async sendLimit(args: ResolveParams<T['params']>): Promise<ISendResponse<any>> {
await new Promise((resolve) => {
if (typeof this.api.limit === 'undefined' || this.api.limit <= 0)
return resolve(1);
if (this.limitSet < this.api.limit) {
this.limitSet += 1;
return resolve(1);
}
this.argsQueue.push(resolve);
});
try {
return await this.cacheHandler(args);
} finally {
this.limitSet -= 1;
this.startNext();
}
}
private hangerBefore(args: ResolveParams<T['params']>) {
for (const beforeListElement of this.beforeList) {
if (beforeListElement) {
args = beforeListElement(args) as ResolveParams<T['params']>;
}
}
return args;
}
private hangerComplete(res: Promise<ISendResponse<any>>): Promise<ISendResponse<any>> {
return this.completeList.reduce((c, p) => {
if (p)
return p(c);
return c;
}, res)
}
private hangerAfter(res: Promise<SendHandlerResponse>): Promise<SendHandlerResponse> {
return this.afterList.reduce((c, p) => {
if (p)
return p(c);
return c;
}, res)
}
private hangerGuard(args: ResolveParams<T['params']>, info: object): Promise<ISendResponse<any>> {
const start = this.guardList.reduceRight((next, c) => {
if (c)
return (a) => c(a, info, next);
return next;
}, ((args) => this.sendLimit(args)) as Hanger<T>['guard'])
if (start)
return start(args)
return this.sendLimit(args);
}
public async sendHandler(args: ResolveParams<T['params']>, responseHandlerFunction?: ResponseHandlerFunction): Promise<SendHandlerResponse> {
const requestTime = new Date();
const info = {
requestTime,
completeTime: new Date()
}
args = this.hangerBefore(args)
const resp = this.hangerComplete(this.hangerGuard(args, info));
return this.hangerAfter(
resp
.catch(e => {
if (e instanceof Error)
this.processResponse(this.fail, responseHandlerFunction?.failFunction, e)
throw e
})
.then(sendResponse => {
info['completeTime'] = new Date()
const isRight = this.expect(sendResponse);
if (isRight) {
this.processResponse(this.success, responseHandlerFunction?.successFunction, sendResponse)
this.saveCacheHandler(args, sendResponse);
return {data: sendResponse.data, info} as SendHandlerResponse
} else {
this.processResponse(this.exception, responseHandlerFunction?.exceptionFunction, sendResponse)
throw new Error(sendResponse.message);
}
})
)
}
protected processResponse(define: Function, custom: Function | undefined | null, ...args: any[]) {
if (typeof custom !== "undefined") {
if (custom != null)
custom.apply(this, args)
} else
define.apply(this, args);
}
private static filteredData(data: GeneralObject): GeneralObject {
return Object.fromEntries(
Object.entries(data).filter(([_, value]) => value !== undefined && value !== null)
);
}
public static toQueryString(data: GeneralObject) {
const queryString = new URLSearchParams(ApiHandler.filteredData(data)).toString();
return queryString.length > 0 ? `?${queryString}` : '';
}
public static toPathString(data: GeneralObject) {
const path = Object.entries(ApiHandler.filteredData(data)).map(([_, value]) => {
return `${value}`
}).join("/")
return ("/" + path).substring(1);
}
public static paramsToObject(params: string[], args: ObjectMapping<string, any>) {
return params.reduce((p, c) => {
p[c] = args[c];
return p;
}, {} as ObjectMapping<string, any>)
}
public static assemblyParameters(params: string[], args: ObjectMapping<string, any> | undefined, api: IApi) {
if (typeof args === 'undefined') return;
const paramsType = api.paramsDefault ?? "query"
const paramsObject = this.paramsToObject(params, args);
if (paramsType === "body")
return paramsObject;
if (paramsType === 'query')
return this.toQueryString(paramsObject);
return this.toPathString(paramsObject);
}
public static assemblyParametersToUnion(params: IApiParamsCollection, args: ObjectMapping<string, any> | undefined, api: IApi) {
if (typeof args === 'undefined') return {
body: {},
query: "",
path: "",
};
const types = Object.entries(params).reduce((p, [k, v]) => {
let path = k;
let paramsType = api.paramsDefault ?? "query"
if (typeof v === 'string') path = v;
else {
path = v.name ?? k;
paramsType = v.type ?? paramsType
}
// @ts-ignore
p[paramsType][path] = args[path];
return p;
}, {
body: {},
query: {},
path: {},
})
return {
body: types.body,
query: this.toQueryString(types.query),
path: this.toPathString(types.path)
}
}
}
export {ApiHandler}