Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

Semaphore

A semaphore limits access to a shared resource with a pool of permits. A one-permit semaphore provides mutual exclusion; a larger pool allows several operations to enter at once. Callers wait without blocking the JavaScript thread when too few permits remain.

Creating a permit pool

function makeUploadLimit() {
  return Semaphore.make(3)
}
// Semaphore.Semaphore!!

Create the pool once for the callers that must share a limit. Allocating a separate pool inside every upload would give each upload its own permits and impose no combined limit. A permit represents access, not a resource value such as a connection.

Limiting concurrent access

function uploadAll() {
  const uploads = Semaphore.make(2)
  return forEach(["a.csv", "b.csv", "c.csv", "d.csv"], (file) => {
    return Semaphore.withPermits(uploads, 1) {
      logInfo("upload started", file)
      sleep("30 millis")
      return file
    }
  }, { concurrency: "unbounded" })
}

The traversal can start all four callers, but only two can enter the permit-protected region at once. This is useful when several unrelated parts of the application must share a resource limit. A traversal's own concurrency option instead limits only that traversal.

Acquiring several permits around work

function weightedJobs() {
  const memoryBudget = Semaphore.make(4)
  return forEach([1, 2, 3], (weight) => {
    return Semaphore.withPermits(memoryBudget, weight) {
      sleep("20 millis")
      return `job using ${weight} units`
    }
  }, { concurrency: 3 })
}

An operation waits until its requested permits can be acquired together. This models jobs with different resource costs, such as memory reservations. Requesting more permits than the pool can supply leaves the operation waiting, so ensure job weights fit the configured pool.

Releasing permits safely

error Rejected

function releaseOnFailure() {
  const mutex = Semaphore.make(1)
  try {
    Semaphore.withPermits(mutex, 1) { throw new Rejected() }
  } catch (error) {
    logWarning("first job rejected", error._tag)
  }
  return pipe(() => "next job can enter", Semaphore.withPermits(mutex, 1))
}

withPermits returns acquired permits on success, failure, and interruption. Prefer it to manually pairing Semaphore.take and release, where cancellation between acquisition and cleanup registration can leak permits. Semaphore.withPermit is the single-permit convenience form with the same release guarantee.