Yieldless
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): Semaphore
  • withPermit(semaphore, operation, options): Promise<SafeResult<T, E>>
  • createRateLimiter({ limit, intervalMs }): RateLimiter
  • type 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 a RangeError.
  • acquire() waits until a permit is available or the signal aborts.
  • tryAcquire() returns null instead of waiting.
  • withPermit() always releases the permit in a finally block.
  • 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 });

On this page