UNPKG

api-simplex-handler

Version:

一个api封装解析器

318 lines (281 loc) 12.2 kB
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}