Files
Situation-Awareness-Platfor…/src/utils/degradeInterceptor.ts
2025-03-12 18:41:20 +08:00

360 lines
12 KiB
TypeScript

import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import { getAdapter } from "axios";
import { storageService } from "./apiStorage";
type IRequestStatus = 'timeout' | 'error' | 'success';
// 请求状态表
interface IRequestStatusRecord {
id: string; // 请求匹配到的缓存策略 url
status: IRequestStatus; // 请求状态
updatedAt: number; // 请求状态更新时间
}
const DEBUG_FLAG = 'common-degrade-interceptor-debug';
const REQUEST_STATUS_TABLE_KEY = 'common-request-status-table';
const PARAM_REG = '[\%0-9a-zA-Z-_.]+';
function debug(...args: any) {
const debugMode = localStorage.getItem(DEBUG_FLAG) === 'true';
if (debugMode) {
console.log('[gitcode]', ...args);
}
}
export class DegradeInterceptor {
private storageService;
constructor() {
this.storageService = storageService;
}
onRequestFulfilled(config: AxiosRequestConfig) {
// 是否走缓存策略?
if (this.disableDegrade() || !this.isReqMatched(config)) {
return config;
}
const strategy = this.getReqCacheStrategy(config);
if (strategy?.timeout) {
config.timeout = strategy.timeout;
}
debug('当前请求', config.method, config.url);
debug('缓存策略', strategy);
if (this.isNeedReadCache(config)) {
debug('降级状态下读缓存');
config.adapter = this.readCacheAdapter.bind(this);
return config;
}
config.adapter = this.retryAdapterEnhancer(config).bind(this);
return config;
}
disableDegrade() {
const disableDegradeFeat = localStorage.getItem('disableDegradeFeat') && localStorage.getItem('disableDegradeFeat')?.toLowerCase() === 'true';
debug('降级开关:', disableDegradeFeat);
return disableDegradeFeat;
}
onResponseFulfilled(response: AxiosResponse) {
if (!this.disableDegrade() && !response?.request?.isCache && this.isReqMatched(response.config)) {
this.handleResp(response);
}
return response;
}
onResponseRejected(error: any) {
if (!this.disableDegrade() && error.config && this.isReqMatched(error.config)) {
// 状态码屏蔽
const strategy = this.getReqCacheStrategy(error.config);
if (strategy?.excludeStatusCode?.includes(error?.request?.status)) {
debug(`当前错误码为${error?.request?.status}不读缓存`);
return Promise.reject(error);
}
const isTimeout = error?.request?.status === 504 || error.code === 'ECONNABORTED';
const errorStatus = isTimeout ? 'timeout' : 'error';
this.setRequestCacheStatus(error.config, errorStatus);
debug('错误状态下读缓存');
return this.getRequestCache(error.config);
}
return Promise.reject(error);
}
handleResp(response: AxiosResponse) {
// 请求没有报错,更新缓存并记录缓存的时间戳
this.setRequestCache(response.config, response);
this.setRequestCacheStatus(response.config, 'success');
// 判断缓存的条数
this.checkRequestCacheRows(response.config);
}
checkRequestCacheRows(request: any) {
const statusList = this.getAllRequestStatus();
const strategy = this.getReqCacheStrategy(request);
const maxRows = strategy.maxRows || 10;
// 先清理一波过期缓存?
const reqCacheRecord = Object.keys(statusList)
.reduce((p, c) => {
p.push({ storageKey: c, ...statusList[c] } as never);
return p;
}, [])
.filter((item: IRequestStatusRecord) => item.id === strategy.url && item.status === 'success' && item.updatedAt)
.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
debug('reqCacheRecord', reqCacheRecord);
if (reqCacheRecord.length < maxRows) {
return;
}
reqCacheRecord.filter((_, index) => index + 1 > maxRows)
.forEach((item: any) => {
debug('删除超限缓存', item.storageKey);
this.storageService.removeItem(item.storageKey);
delete statusList[item.storageKey];
})
this.saveAllRequestStatus(statusList);
}
isReqMatched(config: AxiosRequestConfig): boolean {
const strategy = this.getReqCacheStrategy(config);
const method = strategy?.method || 'get';
const isMatchMethod = config.method?.toLocaleLowerCase() === method.toLocaleLowerCase();
return strategy && isMatchMethod;
}
wrapHttpResponse(config: AxiosRequestConfig, responseCache: any) {
return {
...responseCache,
config,
request: {
...responseCache?.request,
isCache: true,
},
};
}
getStorageKey(config: AxiosRequestConfig) {
const method = config.method?.toLocaleUpperCase();
const strategy = this.getReqCacheStrategy(config);
// 处理 url 尾部参数
const requestParams = this.getRequsetParams(config);
const paramsStringArr: any = [];
Object.keys(requestParams).forEach((key) => {
if (!strategy?.ignoreUrlParams?.includes(key) && key !== '_') {
const value = requestParams[key];
paramsStringArr.push(`${key}=${value}`);
}
});
// 处理 url 内参数
let shortUrl = config.url?.split('?')[0] || '';
const matches: any = this.getReqUrlMatchResult(strategy.url, shortUrl);
Object.keys(matches?.groups ?? {}).forEach((key) => {
if (strategy?.ignoreUrlParams?.includes(key)) {
const value = matches.groups[key];
// 将 /abc123/xxx 替换成 /{serviceId}/xxx
shortUrl = shortUrl.replace(value, `{${key}}`);
}
});
// 处理 headers 参数
const headerString = strategy?.withHeaders?.map((header: any) => config.headers?.[header]).join('&');
const headerStr = headerString ? `${headerString}~` : '';
const paramsString = paramsStringArr.length ? '?' + paramsStringArr.join('&') : '';
return method + '~' + headerStr + shortUrl + paramsString;
}
onRequestRejected = (error: AxiosError) => {
return Promise.reject(error)
}
getRequsetParams(config: AxiosRequestConfig) {
if (config.method === 'get') {
try {
const idx = config.url?.indexOf('?') || -1;
if (idx === -1) {
return config.params || {};
}
const params = config.url?.slice(idx + 1);
const requestParams = params?.split('&').reduce((obj: any, curr) => {
const [key = '', value = ''] = curr?.split('=') || [];
obj[key] = value;
return obj;
}, {});
Object.assign(requestParams, config.params);
return requestParams ?? {};
} catch (error) {
return {};
}
} else {
return typeof config.data === 'string' ? JSON.parse(config.data) : config.data;
}
}
isNeedReadCache(config: AxiosRequestConfig) {
// 降级状态或已经还未过熔断时间,默认为 30s
const reqStatus = this.getRequestStatus(config);
const strategy: any = this.getReqCacheStrategy(config);
const degradedTime = strategy?.degradedTime || 30 * 1000;
const isError = ['timeout', 'error'].includes(reqStatus?.status);
const isInDegradedTime = Date.now() - reqStatus?.updatedAt < degradedTime;
debug('isError', isError, 'isInDegradedTime', isInDegradedTime);
return isError && isInDegradedTime;
}
isCacheTimeout(config: AxiosRequestConfig) {
const strategy: any = this.getReqCacheStrategy(config);
const reqStatus = this.getRequestStatus(config);
if (!strategy.maxAge) {
return false;
}
if (Date.now() - reqStatus.updatedAt > strategy.maxAge) {
this.removeRequestCache(config);
this.deleteRequestStatus(config);
return true;
}
return false;
}
readCacheAdapter(config: AxiosRequestConfig) {
return this.getRequestCache(config).then((responseCache: any) => {
debug('命中缓存');
return this.wrapHttpResponse(config, responseCache);
});
}
retryAdapterEnhancer(config: AxiosRequestConfig) {
const defaultAdapter: any = getAdapter(config.adapter);
return (config: AxiosRequestConfig) => {
const strategy: any = this.getReqCacheStrategy(config);
const retryCount = strategy.retry ?? 0;
let __retryCount = 0;
const request: any = async () => {
try {
return await defaultAdapter(config);
} catch (error: any) {
if (strategy?.excludeStatusCode?.includes(error?.status)) {
debug(`当前错误码为${error?.status}不需要重试`);
return Promise.reject(error);
}
if (!retryCount || __retryCount >= retryCount) {
return Promise.reject(error);
}
__retryCount++;
return request();
}
};
return request();
};
}
// 请求状态表 start
getAllRequestStatus(): { [key: string]: IRequestStatusRecord } {
return JSON.parse(sessionStorage.getItem(REQUEST_STATUS_TABLE_KEY) || '{}');
}
saveAllRequestStatus(statusList: any) {
sessionStorage.setItem(REQUEST_STATUS_TABLE_KEY, JSON.stringify(statusList));
}
getRequestStatus(config: AxiosRequestConfig): IRequestStatusRecord {
const statusList = this.getAllRequestStatus();
const key = this.getStorageKey(config);
return statusList[key];
}
setRequestCacheStatus(config: AxiosRequestConfig, status: IRequestStatus) {
const key = this.getStorageKey(config);
const strategy: any = this.getReqCacheStrategy(config);
const statusList = this.getAllRequestStatus();
statusList[key] = {
id: strategy.url,
status,
updatedAt: Date.now(),
};
debug(key, '状态', status, '时间:', new Date().toLocaleTimeString('zh-cn'));
this.saveAllRequestStatus(statusList);
}
deleteRequestStatus(config: AxiosRequestConfig) {
const key = this.getStorageKey(config);
const statusList = this.getAllRequestStatus();
delete statusList[key];
this.saveAllRequestStatus(statusList);
}
// 请求状态表 end
// 请求缓存表 start
setRequestCache(config: AxiosRequestConfig, response: AxiosResponse) {
const key = this.getStorageKey(config);
// function cannot be cloned in localForage
this.storageService.setItem(key, JSON.parse(JSON.stringify(response)));
}
getRequestCache(config: AxiosRequestConfig) {
const key = this.getStorageKey(config);
return this.storageService.getItem(key);
}
removeRequestCache(config: AxiosRequestConfig) {
const key = this.getStorageKey(config);
this.storageService.removeItem(key);
}
// 请求缓存表 end
// 请求缓存策略表 start
getReqUrlMatchResult(regexpUrl: string, requestUrl: string) {
// 将 /{serviceId}/xxx 替换成具名正则匹配 /(?<serviceId>[0-9a-z-]+)/xxx
let regExp = regexpUrl;
if (typeof regexpUrl === 'string') {
regExp = regexpUrl.replace(/{/g, '(?<').replace(/}/g, `>${PARAM_REG})`) + '$';
}
const noParamUrl = requestUrl.split('?')[0];
const matches = new RegExp(regExp).exec(noParamUrl);
return matches;
}
getReqCacheStrategy(config: AxiosRequestConfig): any {
const requestCacheList = this.storageService.getRequestCacheList();
return requestCacheList
.filter((item: any) => item.url && (typeof item.url === 'string' ? item.url?.length > 2 : true))
.find((item: any) => {
const matches = this.getReqUrlMatchResult(item.url, config.url || '');
return !!matches;
});
}
// 请求缓存策略表 end
}
const degradeInterceptor = new DegradeInterceptor()
degradeInterceptor.onRequestFulfilled = degradeInterceptor.onRequestFulfilled.bind(degradeInterceptor)
degradeInterceptor.onRequestRejected = degradeInterceptor.onRequestRejected.bind(degradeInterceptor)
degradeInterceptor.onResponseFulfilled = degradeInterceptor.onResponseFulfilled.bind(degradeInterceptor)
degradeInterceptor.onResponseRejected = degradeInterceptor.onResponseRejected.bind(degradeInterceptor)
export default degradeInterceptor