feat(projects): 1、执行、任务、工作日志开发调试;2、增加富文本、附件等支撑
This commit is contained in:
79
src/service/request/dedupe.ts
Normal file
79
src/service/request/dedupe.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
dedupe?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const WRITE_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH']);
|
||||
|
||||
type DedupableConfig = Pick<InternalAxiosRequestConfig, 'method' | 'url' | 'data' | 'params'> & {
|
||||
dedupe?: boolean;
|
||||
};
|
||||
|
||||
function isFormDataLike(value: unknown): boolean {
|
||||
if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
|
||||
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function stableJson(value: unknown): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value !== 'object') return JSON.stringify(value);
|
||||
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
||||
const obj = value as Record<string, unknown>;
|
||||
const keys = Object.keys(obj).sort();
|
||||
return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;
|
||||
}
|
||||
|
||||
export function computeDedupeKey(config: DedupableConfig): string | null {
|
||||
const method = (config.method ?? 'GET').toUpperCase();
|
||||
if (!WRITE_METHODS.has(method)) return null;
|
||||
if (config.dedupe === false) return null;
|
||||
if (isFormDataLike(config.data)) return null;
|
||||
|
||||
const url = config.url ?? '';
|
||||
const paramsPart = stableJson(config.params);
|
||||
const bodyPart = stableJson(config.data);
|
||||
return `${method}|${url}?${paramsPart}|${bodyPart}`;
|
||||
}
|
||||
|
||||
const DEFAULT_TTL_MS = 30_000;
|
||||
|
||||
export interface WithDedupeOptions {
|
||||
ttlMs?: number;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
type AnyRequestFn = (...args: any[]) => Promise<unknown>;
|
||||
|
||||
export function withDedupe<TFn extends AnyRequestFn>(request: TFn, options: WithDedupeOptions = {}): TFn {
|
||||
const ttl = options.ttlMs ?? DEFAULT_TTL_MS;
|
||||
const now = options.now ?? Date.now;
|
||||
const pending = new Map<string, { promise: Promise<unknown>; expiresAt: number }>();
|
||||
|
||||
return new Proxy(request, {
|
||||
apply(target, thisArg, args: Parameters<TFn>) {
|
||||
const [config] = args;
|
||||
const key = computeDedupeKey(config as DedupableConfig);
|
||||
if (key === null) return Reflect.apply(target, thisArg, args);
|
||||
|
||||
const cached = pending.get(key);
|
||||
if (cached && cached.expiresAt > now()) return cached.promise;
|
||||
if (cached) pending.delete(key);
|
||||
|
||||
const promise = Promise.resolve()
|
||||
.then(() => Reflect.apply(target, thisArg, args))
|
||||
.finally(() => {
|
||||
const current = pending.get(key);
|
||||
if (current && current.promise === promise) {
|
||||
pending.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
pending.set(key, { promise, expiresAt: now() + ttl });
|
||||
return promise;
|
||||
}
|
||||
}) as TFn;
|
||||
}
|
||||
Reference in New Issue
Block a user