Something went wrong. Try again.
Mirrored from GitHub github.com/roostorg/coop
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091import { type Logger } from '../types/index.js';import { defaultLoggersByComponent } from '../utils/utils.js';
/** * Imagine you have an function that, when called, kicks off a task, and * returns a promise representing the result of the task. Now, suppose the task * is expensive, but not effectful, like fetching from a REST API. If your code * attempts to run this task many times in quick succession, or many times in * parallel, this can be wasteful (if the result will always or likely be the * same) and/or overload whatever's doing work as part of the task (eg the API). * * There are a large number of npm packages that make it easy to better schedule * this work (e.g., queueing some parallel runs of the task behind others, if * the number of tasks already running exceeds some concurrency limit) or to * reuse the results of some runs of the task for others (by debouncing or * throttling calls to the function that starts the task). However, none of them * have quite the -- admittedly a bit quirky -- semantics that we want, and that * we implement here. * * Specifically, this function accepts a function for starting/creating tasks, * along with some options, and returns a new function that behaves like the * original except that the promise resulting from a call is reused on * subsequent calls if and only if: 1) the subsequent calls have the same * arguments [a la memoize]; 2) less than a certain number of milliseconds have * passed [a la throttle]; and 3) the original task is still running [unique]. * In other words, it memoizes the original task-starting function, but then * reverts to calling through to the original function after a certain number * of milliseconds or after the last-started task finishes, whichever is first. * So it's sort of a combination of memoize, throttle, and promise chaining. * * @param taskCreator The task creation function * @param collapseTasksMs The number of milliseconds up until which, if a caller * tries to start the same task while a previous version of the task is still * running, the promise for the result of the currently-running task will be * returned instead. * @param key A function for converting the arguments passed to the task * creation into a cache key, like in your standard memoize implementation. */export default function collapsedTaskCreator<Args extends unknown[], Result>( taskCreator: (...args: Args) => Promise<Result>, collapseTasksMs: number, // TODO: think about how to make this whole key generation process type safe. // E.g., how to verify that, if JSON.stringify is used, the value is properly // stringifyable? Would it be better to just have no default key option? key: (args: Args) => any = JSON.stringify.bind(JSON), logger: Logger = defaultLoggersByComponent['collapsed-task-creator'],) { // Tuple of [PromiseForTaskResult, taskStartTimestamp]. const pendingTasks = new Map<any, [Promise<Result>, number]>(); const logTrace = logger.bind(null, 'collapsed-task-creator', 'trace');
return async (...args: Args) => { const taskKey = key(args); const res = pendingTasks.get(taskKey); const now = Date.now(); logTrace('requested = new state for taskKey/args', { args, taskKey });
if (!res || now - res[1] > collapseTasksMs) { logTrace( res ? "started new task; there _was_ an in-progress one, but it's too old" : 'started new task b/c there was no in-progress task for these args', args, );
const taskRes = taskCreator(...args).finally(async () => { // Only remove this task from pendingTasks if pendingTasks[taskKey] // is still the same task. (It could be a new one if the old one was // overwritten for taking longer than collapseTasksMs.) const pendingValueNow = pendingTasks.get(taskKey); if (pendingValueNow && pendingValueNow[0] === taskRes) { logTrace('completed = new state for taskKey/args', { args, taskKey }); pendingTasks.delete(taskKey); } });
// Save the new task as a pending task. This will be _replacing_ // an existing pending task for this key if the other was too old. pendingTasks.set(taskKey, [taskRes, now]); logTrace('pending = new state for taskKey/args', { args, taskKey }); return taskRes; }
logTrace( 'reusing result from prior, still-in-progress run of task for args/taskKey', { args, taskKey }, ); return res[0]; };}