yieldless/singleflight
Deduplicate concurrent tuple work by key.
yieldless/singleflight prevents duplicate in-flight work from stampeding the same expensive operation. Calls with the same key share one promise while it is running, then the entry is removed when it settles.
This is useful for Electron preload APIs, CLIs, API clients, docs search, and cache warmers where several callers can ask for the same thing at once.
Exports
singleFlight(operation, options): SingleFlighttype SingleFlightOperation<Args, T, E = Error> = (signal, ...args) => SafeResult<T, E> | PromiseLike<SafeResult<T, E>>type SingleFlightOptions<Args> = { getKey?, signal? }type SingleFlight = callable & { clear(...args), clearAll(), has(...args), size }
Example
import { singleFlight } from "yieldless/singleflight";
const loadRepository = singleFlight(
async (signal, repoId: string) => readRepository(repoId, signal),
);
const [first, second] = await Promise.all([
loadRepository("yieldless"),
loadRepository("yieldless"),
]);Only one readRepository() call runs for the duplicate key. Both callers receive the same tuple result.
Custom keys
The default key is JSON.stringify(args). Pass getKey when your arguments include values that need a stable domain key.
const loadUser = singleFlight(
async (signal, request: { userId: string; refresh: boolean }) =>
readUser(request.userId, signal),
{
getKey: (request) => request.userId,
},
);Behavior notes
- Results are not cached after settlement.
- Thrown operation failures are normalized into tuple errors.
clear(...args)aborts and removes one in-flight entry.clearAll()aborts and removes every in-flight entry.- A parent
signalaborts the shared operation for every active caller.
Good
Wrap expensive idempotent reads that often get requested twice at the same time.
const loadPullRequest = singleFlight(
(signal, owner: string, repo: string, number: number) =>
fetchPullRequest(owner, repo, number, signal),
{
getKey: (owner, repo, number) => `${owner}/${repo}#${String(number)}`,
},
);Clear entries when a view or process is no longer interested.
loadPullRequest.clear(owner, repo, number);Avoid
Do not use singleFlight() as a long-lived cache.
const loadUser = singleFlight(readUser);
// Later, expecting this to be cached:
await loadUser("same-user");Entries are removed when the in-flight promise settles. Put durable caching in your own data layer.