UNPKG

@cloudcome/utils-core

Version:
1 lines 14.1 kB
{"version":3,"file":"async.mjs","sources":["../src/async.ts"],"sourcesContent":["import { fnNoop } from './function';\nimport type { AnyArray, AnyAsyncFunction } from './types';\n\n/**\n * 表示异步任务的类型\n * @template T - 任务返回值的类型\n */\ntype AsyncTask<T> = {\n /** 任务索引 */\n idx: number;\n /** 异步任务函数 */\n afn: () => Promise<T>;\n /** Promise 的解析器对象 */\n pwr?: PromiseWithResolvers<T>;\n};\n\n/**\n * 异步任务队列的配置选项\n */\nexport type AsyncQueueOptions = {\n /**\n * 并发限制数,0 表示无限制\n * @default 0\n */\n limit?: number;\n};\n\n/**\n * 异步任务队列,用于管理和控制异步任务的执行\n * @template T - 任务返回值的类型\n */\nexport class AsyncQueue<T> {\n #tasks: AsyncTask<T>[] = [];\n #length = 0;\n\n /**\n * 创建一个异步任务队列\n * @param asyncFns - 要执行的异步函数数组\n * @param options - 队列配置选项\n */\n constructor(\n asyncFns: Array<() => Promise<T>>,\n readonly options?: AsyncQueueOptions,\n ) {\n asyncFns.forEach((afn, idx) => {\n this.#add('push', afn);\n });\n }\n\n get length() {\n return this.#length;\n }\n\n get limit() {\n return this.options?.limit || 0;\n }\n\n #add(method: 'unshift' | 'push', afn: () => Promise<T>, pwr?: PromiseWithResolvers<T>) {\n this.#tasks[method]({\n idx: this.#length++,\n afn: afn,\n pwr: pwr,\n });\n }\n\n #addAndRun(method: 'unshift' | 'push', afn: () => Promise<T>) {\n // 明确终止了\n if (this.#stopLength >= 0) {\n throw new Error('异步队列已被终止,无法添加新的任务');\n }\n\n const pwr = Promise.withResolvers<T>();\n this.#add(method, afn, pwr);\n\n if (this.#startPwr && this.#running === 0) {\n this.#run();\n }\n\n return pwr.promise;\n }\n\n async push(afn: () => Promise<T>) {\n return this.#addAndRun('push', afn);\n }\n\n async unshift(afn: () => Promise<T>) {\n return this.#addAndRun('unshift', afn);\n }\n\n #startResolved = 0;\n #startRejected = 0;\n get startSettled() {\n return this.#startResolved === this.#startLength || this.#startRejected > 0;\n }\n\n #startResults: T[] = [];\n #startPwr: PromiseWithResolvers<T[]> | null = null;\n #startLength = 0;\n\n /**\n * 启动队列中的任务执行\n * @returns 返回一个 Promise,在所有启动任务完成后解析为结果数组\n */\n async start(): Promise<T[]> {\n if (this.#startPwr) return this.#startPwr.promise;\n\n // 固化启动时长度,便于判断 start 异步结果\n this.#startLength = this.#length;\n this.#startPwr = Promise.withResolvers<T[]>();\n\n if (this.#startLength === 0) {\n this.#startPwr.resolve([]);\n } else {\n this.#run();\n }\n\n return this.#startPwr.promise;\n }\n\n #running = 0;\n #run() {\n while (this.limit === 0 || this.#running < this.limit) {\n const task = this.#tasks.shift();\n\n // 无任务可执行\n if (!task) break;\n\n this.#running++;\n\n task\n .afn()\n .then((result) => {\n this.#running--;\n task.pwr?.resolve(result);\n\n // 属于启动任务\n if (task.idx < this.#startLength) {\n this.#startResults[task.idx] = result;\n this.#startResolved++;\n }\n\n // 所有启动任务都已执行完毕\n if (this.#startResolved === this.#startLength) {\n this.#startPwr?.resolve(this.#startResults);\n }\n\n this.#stopResults[task.idx] = result;\n this.#stopResolved++;\n\n // 所有停止任务都已执行完毕\n if (this.#stopResolved === this.#stopLength) {\n this.#stopPwr?.resolve(this.#stopResults);\n } else {\n this.#run();\n }\n })\n .catch((reason) => {\n this.#running--;\n task.pwr?.reject(reason);\n\n // 属于启动任务\n if (task.idx < this.#startLength) {\n this.#startRejected++;\n this.#startPwr?.reject(reason);\n }\n\n // 属于停止任务\n if (this.#stopLength > 0) {\n this.#stopRejected++;\n this.#stopPwr?.reject(reason);\n }\n });\n }\n }\n #stopResolved = 0;\n #stopRejected = 0;\n get stopSettled() {\n return this.#stopResolved === this.#stopLength || this.#stopRejected > 0;\n }\n\n #stopLength = -1;\n #stopResults: T[] = [];\n\n #stopPwr?: PromiseWithResolvers<T[]> | null = null;\n\n /**\n * 终止队列中的任务执行,终止队列后不再可以追加异步任务\n * @returns 返回一个 Promise,在所有启动任务完成后解析为结果数组\n */\n async stop() {\n if (this.#stopPwr) {\n return this.#stopPwr.promise;\n }\n\n this.#stopLength = this.#length;\n this.#stopPwr = Promise.withResolvers<T[]>();\n\n if (this.#stopLength === 0) {\n this.#stopPwr.resolve([]);\n } else {\n this.#run();\n }\n\n return this.#stopPwr.promise;\n }\n}\n\n/**\n * 使用给定的并发限制执行异步函数\n *\n * 此函数的目的是控制一组异步函数的并发执行数量,通过创建一个AsyncQueue实例来管理这些异步函数的执行\n * 它确保在任何给定时间只有最多`limit`数量的异步函数被执行,以避免潜在的性能问题或资源竞争\n *\n * @param asyncFns 一个包含异步函数的数组,每个异步函数都不需要参数,并返回一个Promise\n * @param limit 并发限制的数量,表示同时执行的异步函数的最大数量,0 表示不限制\n * @returns 返回一个Promise,当所有异步函数都执行完毕后,该Promise将被解析\n */\nexport function asyncLimit<T>(asyncFns: Array<() => Promise<T>>, limit: number) {\n const aq = new AsyncQueue<T>(asyncFns, { limit });\n return aq.start();\n}\n\n/**\n * 异步共享函数的配置选项\n */\nexport type AsyncSharedOptions<I extends AnyArray, O> = {\n /**\n * 是否在调用结束后再执行(只在运行期间有再次调用时才会生效)\n * @type {boolean}\n * @default false\n * @example\n * const sharedFn = asyncShared(fetchData, { trailing: true });\n * // 如果在 fetchData 执行期间多次调用 sharedFn,则会在 fetchData 结束后再次执行\n */\n trailing?: boolean;\n\n /**\n * 缓存结果的最大有效期(毫秒)\n * @type {number}\n * @example\n * const sharedFn = asyncShared(fetchData, { maxAge: 1000 });\n * // 在 1 秒内调用 sharedFn 会直接返回缓存结果\n */\n maxAge?: number;\n\n /**\n * 在调用共享函数时触发的回调函数\n * @param inputs - 传递给共享函数的参数\n * @example\n * const options: AsyncSharedOptions<typeof fetchData> = {\n * onTrigger: (...args) => console.log('Calling with:', args)\n * };\n */\n onTrigger?: (...inputs: I) => unknown;\n\n /**\n * 在执行异步函数时触发的回调函数\n * @param args - 传递给异步函数的参数\n * @example\n * const options: AsyncSharedOptions<typeof fetchData> = {\n * onExecute: (...args) => console.log('Executing with:', args)\n * };\n */\n onExecute?: (...args: I) => unknown;\n\n /**\n * 在异步函数成功执行后触发的回调函数\n * @param output - 异步函数的返回结果\n * @example\n * const options: AsyncSharedOptions<typeof fetchData> = {\n * onSuccess: (result) => console.log('Success:', result)\n * };\n */\n onSuccess?: (output: O) => unknown;\n\n /**\n * 在异步函数执行失败时触发的回调函数\n * @param error - 异步函数抛出的错误\n * @example\n * const options: AsyncSharedOptions<typeof fetchData> = {\n * onError: (error) => console.error('Error:', error)\n * };\n */\n onError?: (error: unknown) => unknown;\n\n /**\n * 在异步函数执行完成(无论成功或失败)时触发的回调函数\n * @example\n * const options: AsyncSharedOptions<typeof fetchData> = {\n * onFinally: () => console.log('Execution completed')\n * };\n */\n onFinally?: () => unknown;\n};\n\n/**\n * 创建一个共享执行结果的异步函数\n * @template F - 异步函数类型\n * @param {F} af - 要共享的异步函数\n * @param {AsyncSharedOptions} [options] - 配置选项\n * @returns {F} 返回一个新的异步函数,该函数会共享执行结果\n * @example\n * const fetchData = async (id) => {\n * // 模拟异步操作\n * return await fetch(`/api/data/${id}`);\n * };\n *\n * const sharedFetch = asyncShared(fetchData, { maxAge: 1000 });\n *\n * // 多次调用会共享同一个请求\n * const result1 = await sharedFetch(1);\n * const result2 = await sharedFetch(1); // 上次请求完成后 1000ms 内直接返回缓存结果\n */\nexport function asyncShared<I extends AnyArray, O>(\n af: (...inputs: I) => Promise<O>,\n options?: AsyncSharedOptions<I, O>,\n) {\n let executedPromise: Promise<O> | undefined;\n let executing = false;\n let executingInputs: I | undefined;\n let executedTime = 0;\n\n const _sharedAf = async (from: 'trigger' | 'trailing', ...inputs: I) => {\n executingInputs = inputs;\n\n // 如果正在运行,则复用运行结果\n if (executing && executedPromise) {\n return executedPromise;\n }\n\n // 如果已运行结束空闲时,判断是否在等待时间内\n if (executedPromise && Date.now() - executedTime < (options?.maxAge || 0)) {\n return executedPromise;\n }\n\n // 否则直接执行\n executing = true;\n options?.onExecute?.(...executingInputs);\n executedPromise = af(...executingInputs);\n executingInputs = undefined;\n executedPromise\n .then((res) => {\n options?.onSuccess?.(res);\n })\n .catch((err) => {\n options?.onError?.(err);\n })\n .finally(() => {\n executing = false;\n executedTime = Date.now();\n options?.onFinally?.();\n\n // 执行期间多次调用,则重新执行\n if (executingInputs && options?.trailing) {\n _sharedAf('trailing', ...executingInputs);\n }\n });\n\n return executedPromise;\n };\n\n return function sharedAf(...inputs: I): Promise<O> {\n options?.onTrigger?.(...inputs);\n const p = _sharedAf('trigger', ...inputs);\n // 必须捕获错误,否则单测错误边界时会抛错\n p.catch(fnNoop);\n return p;\n };\n}\n\n// const af1 = asyncShared(async () => {\n// return 1;\n// });\n// const n = await af1();\n\n// const af2 = asyncShared(async (a: number) => {\n// return a + 1;\n// });\n// const n2 = await af2(2);\n"],"names":[],"mappings":"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"}