Reference
yieldless/limiter
Semaphores and rate limiters for protecting shared async boundaries.
yieldless/limiter keeps pressure explicit. It gives you a tiny semaphore for local concurrency and a simple rate limiter for APIs that need paced calls. Both are built on promises and AbortSignal.
Use it when many independent flows share a limited resource: subprocess slots, database connections, API quota, filesystem pressure, or CPU-heavy local work.
Exports
createSemaphore(capacity): SemaphorewithPermit(semaphore, operation, options): Promise<SafeResult<T, E>>createRateLimiter({ limit, intervalMs }): RateLimitertype Semaphore = { acquire, tryAcquire, withPermit, available, capacity, pending }type SemaphorePermit = { release, [Symbol.dispose] }type RateLimiter = { take, takeSafe, clear, pending }
Example
import { createSemaphore, withPermit } from "yieldless/limiter";
import { safeTry } from "yieldless/error";
const gitSlots = createSemaphore(3);
const [error, result] = await withPermit(
gitSlots,
(signal) => safeTry(runGitCommand(repoPath, ["status"], { signal })),
{ signal },
);Pace API calls:
import { createRateLimiter } from "yieldless/limiter";
const githubLimit = createRateLimiter({
limit: 2,
intervalMs: 1_000,
});
await githubLimit.take({ signal });
const response = await fetch(url, { signal });Behavior notes
createSemaphore(0)throws aRangeError.acquire()waits until a permit is available or the signal aborts.tryAcquire()returnsnullinstead of waiting.withPermit()always releases the permit in afinallyblock.- The exported
withPermit()helper expects tuple-returning work and normalizes acquisition or thrown failures into tuple errors. createRateLimiter()schedules calls into fixed-size windows.rateLimiter.clear(reason)rejects pending waiters.
Good
Protect the boundary, not every call site.
const subprocesses = createSemaphore(4);
export function runLimitedGit(args: readonly string[], signal: AbortSignal) {
return withPermit(
subprocesses,
(permitSignal) => runCommandSafe("git", args, { signal: permitSignal }),
{ signal },
);
}Use takeSafe() when rate-limit waiting is part of a tuple flow.
const [limitError] = await githubLimit.takeSafe({ signal });
if (limitError) return [limitError, null] as const;Avoid
Do not acquire a permit and forget to release it.
const permit = await semaphore.acquire();
await doWork();Prefer scoped release.
await semaphore.withPermit((signal) => doWork(signal), { signal });