import type { KeyResolver } from "./singleFlight"; /** * 合并包装的配置项。 */ export interface CoalesceOptions { /** * 从入参派生并发槽位的 key。不同 key 互不影响。 * 不传则所有调用共用一个槽位。 */ key?: KeyResolver; /** * 每一轮执行失败时的回调。 * * 本函数返回的 Promise **永远不会 reject**(详见函数说明), * 想观察失败必须通过这个回调。 */ onError?: (error: unknown, args: Args) => void; } /** * 合并包装后的函数类型,附带若干只读的状态查询。 */ export type CoalesceFunction = ((...args: Args) => Promise) & { /** 对应槽位当前是否有执行在进行中。 */ isRunning(...args: Args): boolean; /** 对应槽位当前是否已排了一次补跑。 */ hasPending(...args: Args): boolean; /** 进行中的槽位数量。 */ readonly size: number; }; /** * 创建一个「合并尾部」函数:执行期间的多次触发合并成一次补跑。 * * 首次调用立即执行。执行期间再次触发时,既不丢弃也不共享结果,而是记一个 * 标记;当前这轮结束后,用**最新一次**的入参再跑一遍。期间触发多少次都只 * 补跑一次 —— 这就是「合并」。 * * ## 什么时候需要它 * * 当**触发本身意味着「状态变了」,而进行中那轮读到的是旧状态**时: * * ```text * t0 状态 = A → 触发 → 开始跑,读到 A * t1 状态 = B → 触发 * t2 这一轮跑完,结果基于 A * ``` * * - {@link singleExecutionWrapper}:t1 被丢弃 → **B 永久丢失** * - {@link singleFlight}:t1 拿到基于 A 的结果 → 同样丢失 B * - 本函数:t1 记标记 → t2 之后补跑一次,读到 B * * 前两者的隐含前提是「这一轮的结果对后来者同样成立」。一旦触发携带新信息, * 这个前提就不成立了。 * * ## 与 debounce 的区别 * * 两者正好相反:`debounce` 把首次响应**延迟**到静默期之后;本函数首次 * **立即**执行,把合并放在尾部。二者可以叠加 —— 先用 `debounce` 压掉密集 * 触发,再用本函数保证「跑的时候状态又变了」不丢。 * * ## 错误处理 * * 返回的 Promise **永远不会 reject**,某一轮失败也不会中断后面的补跑 —— * 这个原语的目的是「最终收敛到最新状态」,一次失败不该让整条链停摆。 * 想观察失败请传 `onError`。 * * 这个取舍也避免了事件监听器里的常见坑: * `listener(() => syncUser())` 若返回会 reject 的 Promise 且无人接手, * 就会冒出 unhandled rejection。 * * ⚠️ 被包装的函数不能通过包装后的入口调用自己,会无限补跑下去。 * * @param func - 需要包裹的函数,同步函数也可以。它的返回值会被忽略。 * @param options - 配置项,可通过 `key` 按入参分组、`onError` 观察失败。 * @returns 包装后的函数。返回的 Promise 在「本次触发已被某一轮执行覆盖」 * 之后 resolve:首次调用等自己这轮,被合并的调用等到补跑结束。 * * @example * ```ts * const syncState = coalesce(async () => { * const latest = await readState(); * await push(latest); * }); * * // 密集触发时:立即跑一轮,期间的触发合并成结束后的一次补跑, * // 保证最后一次状态变化一定被处理到 * store.subscribe(() => syncState()); * ``` */ export declare function coalesce(func: (...args: Args) => unknown, options?: CoalesceOptions): CoalesceFunction;