* -------------------------------------------------------------------------
* This file is part of the MindStudio project.
* Copyright (c) 2026 Huawei Technologies Co.,Ltd.
*
* MindStudio is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
* -------------------------------------------------------------------------
*/
* 防抖请求函数接口
* 包含扩展的控制方法
*/
export interface DebouncedRequestFunc<T extends (...args: any[]) => Promise<any>> {
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
cancel(key?: string): void;
flush(key?: string): void;
getPendingCount(): number;
getPendingKeys(): string[];
}
* 智能防抖高阶函数
*
* 两种模式:
* - trailing(默认):等待期间更新参数,延迟结束后发送最终参数的请求
* - leading:第一次调用立即执行,delay 窗口内所有调用方统一等待,窗口结束时统一返回第一次的结果
*
* ⚠️ 内存泄漏警告:
* 本函数内部使用 Map<string, KeyState> 维护每个 key 的状态。状态仅在请求完成/取消/flush 后清理。
* 若调用方创建了带有不同 key 的大量请求但从未等待其完成(如未 await 且不调用 cancel/flush),
* 对应的状态对象会持续占用内存。建议:
* - 确保所有调用方 await 返回的 Promise 或妥善处理 rejection
* - 组件卸载/页面关闭时调用 cancel() 清理所有待处理状态
* - 长时间运行的场景下定期调用 getPendingCount() 监控
*
* 状态流转:
*
* trailing 模式:
* ```
* idle ──debounced('a')──► pending
* │
* │ delay 内 debounced('b') → 替换参数,重置 timer
* │
* ▼
* inflight ──请求完成──► idle
* │
* │ delay 内 debounced('b') → 新 state 重新进入 pending
* │
* ▼
* pending ──timer到期──► inflight
* ```
*
* leading 模式:
* ```
* idle ──debounced('a')──► inflight(立即执行请求)
* │
* │ delay 内 debounced('b/c') → 加入队列,不触发新请求
* │
* ├───请求在 delay 内完成────┐
* │ │
* ▼ │
* delay到期 → 统一 resolve 队列 │
* │ │
* ▼ │
* idle ◄──────────────────────┘
* │
* └───请求耗时 > delay────────┐
* │
* delay到期 → past_delay │
* │ │
* ▼ │
* 请求完成 → 立即 resolve 队列 │
* │ │
* ▼ │
* idle ◄───────────────────────┘
* ```
*
* @param requestFn - 原始请求函数 (params) => Promise<any>
* @param options - 配置选项
* @returns 包装后的防抖函数
*/
export function createSmartDebounceRequestFunc<T extends (...args: any[]) => Promise<any>>(
requestFn: T,
options?: {
delay?: number;
keyFn?: (...args: Parameters<T>) => string;
leading?: boolean;
onBeforeRequest?: (...args: Parameters<T>) => void;
onAfterRequest?: (result: Awaited<ReturnType<T>>, ...args: Parameters<T>) => void;
}
): DebouncedRequestFunc<T> {
type Resolve = (value: Awaited<ReturnType<T>>) => void;
type Reject = (reason?: any) => void;
type QueueItem = { resolve: Resolve; reject: Reject };
* 内部状态对象
*
* 通用字段:
* - status : 当前状态
* - timer : setTimeout 句柄(trailing 的 pending 定时器 / leading 的 delay 定时器)
* - args : 最后一次调用传入的参数
*
* trailing 模式专用:
* - trailingResolve / trailingReject : pending/inflight 时存储单次调用的 resolve/reject
*
* leading 模式专用:
* - leadingQueue : delay 窗口内所有调用方的 Promise 回调队列
* - leadingResult : 请求成功时缓存的结果
* - leadingError : 请求失败时缓存的错误
* - leadingCompleted : 请求是否已完成
*/
type KeyState = {
status: 'idle' | 'pending' | 'inflight' | 'past_delay';
timer?: NodeJS.Timeout;
args?: Parameters<T>;
trailingResolve?: Resolve;
trailingReject?: Reject;
leadingQueue: QueueItem[];
leadingResult?: Awaited<ReturnType<T>>;
leadingError?: any;
leadingCompleted: boolean;
};
const {
delay = 300,
keyFn,
leading = false,
onBeforeRequest,
onAfterRequest
} = options || {};
const states = new Map<string, KeyState>();
const resolveLeadingQueue = (state: KeyState) => {
while (state.leadingQueue.length > 0) {
const { resolve } = state.leadingQueue.shift()!;
resolve(state.leadingResult as Awaited<ReturnType<T>>);
}
};
const rejectLeadingQueue = (state: KeyState, reason?: any) => {
while (state.leadingQueue.length > 0) {
const { reject } = state.leadingQueue.shift()!;
reject(reason);
}
};
const cleanupLeadingState = (state: KeyState) => {
if (state.leadingError) {
rejectLeadingQueue(state, state.leadingError);
} else {
resolveLeadingQueue(state);
}
};
const onLeadingDelayExpired = (key: string, state: KeyState) => {
const current = states.get(key);
if (!current || current !== state) return;
if (state.leadingCompleted) {
cleanupLeadingState(state);
states.delete(key);
} else {
state.status = 'past_delay';
}
};
const onLeadingRequestDone = (key: string, state: KeyState) => {
const current = states.get(key);
if (!current || current !== state) return;
if (state.status === 'past_delay') {
cleanupLeadingState(state);
states.delete(key);
}
};
const executeLeading = async (key: string, args: Parameters<T>, state: KeyState) => {
try {
onBeforeRequest?.(...args);
const result = await requestFn(...args);
onAfterRequest?.(result, ...args);
const current = states.get(key);
if (current === state) {
state.leadingResult = result;
state.leadingCompleted = true;
onLeadingRequestDone(key, state);
}
} catch (error) {
const current = states.get(key);
if (current === state) {
state.leadingError = error;
state.leadingCompleted = true;
onLeadingRequestDone(key, state);
}
}
};
const executeRequest = async (key: string, args: Parameters<T>, state: KeyState) => {
try {
onBeforeRequest?.(...args);
const result = await requestFn(...args);
onAfterRequest?.(result, ...args);
if (states.get(key) === state && state.status === 'inflight') {
state.trailingResolve?.(result);
states.delete(key);
}
} catch (error) {
if (states.get(key) === state && state.status === 'inflight') {
state.trailingReject?.(error);
states.delete(key);
} else {
state.trailingReject?.(error);
}
}
};
const startTimer = (key: string, state: KeyState) => {
state.timer = setTimeout(() => {
const current = states.get(key);
if (!current || current !== state || current.status !== 'pending') return;
current.status = 'inflight';
current.timer = undefined;
executeRequest(key, current.args!, current);
}, delay);
};
const debouncedFn = (...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> => {
const key = keyFn ? keyFn(...args) : 'default';
return new Promise((resolve, reject) => {
let state = states.get(key);
if (!state) {
state = {
status: 'idle',
leadingQueue: [],
leadingCompleted: false,
};
states.set(key, state);
}
if (leading) {
if (state.status === 'idle') {
state.status = 'inflight';
state.args = args;
state.leadingQueue = [{ resolve, reject }];
state.leadingCompleted = false;
state.leadingResult = undefined;
state.leadingError = undefined;
state.timer = setTimeout(() => onLeadingDelayExpired(key, state!), delay);
executeLeading(key, args, state);
} else if (state.status === 'inflight') {
state.leadingQueue.push({ resolve, reject });
} else if (state.status === 'past_delay') {
const newState: KeyState = {
status: 'inflight',
args,
leadingQueue: [{ resolve, reject }],
leadingCompleted: false,
};
states.set(key, newState);
newState.timer = setTimeout(() => onLeadingDelayExpired(key, newState), delay);
executeLeading(key, args, newState);
}
} else {
if (state.status === 'idle' || state.status === 'pending') {
if (state.timer) clearTimeout(state.timer);
state.status = 'pending';
state.args = args;
state.trailingResolve = resolve;
state.trailingReject = reject;
startTimer(key, state);
} else if (state.status === 'inflight') {
const newState: KeyState = {
status: 'pending',
args,
leadingQueue: [],
leadingCompleted: false,
trailingResolve: resolve,
trailingReject: reject,
};
states.set(key, newState);
startTimer(key, newState);
}
}
});
};
debouncedFn.cancel = (key?: string) => {
const doCancel = (k: string) => {
const state = states.get(k);
if (!state || state.status === 'idle') return;
if (state.timer) clearTimeout(state.timer);
if (leading) {
rejectLeadingQueue(state, new Error('Cancelled'));
} else {
}
states.delete(k);
};
if (key) {
doCancel(key);
} else {
Array.from(states.keys()).forEach(doCancel);
}
};
debouncedFn.flush = (key?: string) => {
const doFlush = (k: string) => {
const state = states.get(k);
if (!state) return;
if (leading) {
if (state.status === 'inflight') {
if (state.timer) clearTimeout(state.timer);
onLeadingDelayExpired(k, state);
}
} else {
if (state.status === 'pending') {
if (state.timer) clearTimeout(state.timer);
state.status = 'inflight';
state.timer = undefined;
executeRequest(k, state.args!, state);
}
}
};
if (key) {
doFlush(key);
} else {
Array.from(states.keys()).forEach(doFlush);
}
};
debouncedFn.getPendingCount = () => {
return Array.from(states.values()).filter(s => s.status !== 'idle').length;
};
debouncedFn.getPendingKeys = () => {
return Array.from(states.entries())
.filter(([, s]) => s.status !== 'idle')
.map(([k]) => k);
};
return debouncedFn;
}