/** * 从入参派生并发槽位的函数。 * * @template Args - 被包装函数的参数类型。 */ export type KeyResolver = (...args: Args) => string; /** * 单飞包装的配置项。 */ export interface SingleFlightOptions { /** * 从入参派生并发槽位的 key。不同 key 互不影响。 * 不传则所有调用共用一个槽位。 */ key?: KeyResolver; } /** * 单飞包装后的函数类型,附带若干只读的状态查询。 */ export type SingleFlightFunction = ((...args: Args) => Promise) & { /** 对应槽位当前是否有调用正在进行中。 */ isRunning(...args: Args): boolean; /** 进行中的槽位数量。 */ readonly size: number; }; /** * 创建一个「单飞」函数:并发调用共享同一次执行。 * * 前一次调用还没结束时,后来的调用者拿到的是**同一个 Promise**, * 因此能拿到结果、也能感知异常。执行结束(无论成败)后槽位立即释放, * 下一次调用会真正执行。 * * 常用于同一份数据被多处同时请求的场景,避免重复发起。 * * ## 与 {@link singleExecutionWrapper} 怎么选 * * 分水岭是**后来的调用方要不要结果**: * 要 → 本函数;不要、且希望它立刻脱身 → `singleExecutionWrapper`。 * * | | `singleExecutionWrapper` | `singleFlight` | * | --- | --- | --- | * | 被挡住的调用 | 立刻返回 `RealUndefined` | 返回进行中那个 Promise,跟着等 | * | 拿得到结果 | 否 | 是 | * | 首次调用**失败**时 | 后来者不受影响 | 后来者一起收到同一个异常 | * | 首次调用**卡住**时 | 后来者立刻返回 | 后来者一起挂住 | * | 能区分「我发起的」 | 能,靠 `RealUndefined` | 不能 | * | 最短锁定时长 `minTime` | 支持 | 不支持 | * * 两条容易被忽略的实际差别: * * - **失败传播范围**:本函数会把一次失败传给 N 个调用方。若每个调用点的 * catch 里都弹提示,一次网络错误会弹 N 次。 * - **级联挂起**:首次调用卡住时,后续调用者会陪着一起挂。高频事件驱动 * (cookie 变化、DOM 变化)的场景下,`singleExecutionWrapper` 让它们 * 立刻脱身通常更合适。 * * 另外两者都**不提供**「合并尾部」语义 —— 进行中时既不丢弃也不共享, * 而是记一个标记、跑完再补一次。触发本身携带新状态时(当前这次跑的是 * 旧状态)需要的是那个,这两个都会丢掉新状态。 * * 本函数只负责「同一时刻」的并发,**不做结果缓存**。跨次调用要不要重复 * 执行属于缓存层的职责,需要缓存请配合 {@link cacheWrapper}。 * * ⚠️ 被包装的函数不能通过包装后的入口调用自己,否则会拿到自己那个尚未 * settle 的 Promise,永远等下去。需要递归时把真正的逻辑抽成内部函数, * 只在最外层入口做包装。 * * @param func - 需要包裹的函数,同步函数也可以。 * @param options - 配置项,可通过 `key` 按入参分组。 * @returns 包装后的函数,附带 `isRunning()` 与 `size`。 * * @example * ```ts * const fetchProfile = singleFlight( * (id: string) => http.get(`/profile/${id}`), * { key: (id) => id } * ); * // 同一个 id 并发调用只发一个请求,两个调用方拿到同一份结果 * const [a, b] = await Promise.all([fetchProfile("x"), fetchProfile("x")]); * ``` */ export declare function singleFlight(func: (...args: Args) => T | Promise, options?: SingleFlightOptions): SingleFlightFunction;