Problem
Semaphore (v4) offers two acquisition styles:
take — manual, pairs with release, but waits for permits
withPermitsIfAvailable — fail-fast, but brackets a single Effect: the permit is released when that effect exits
There is no way to acquire a permit fail-fast and keep holding it beyond one effect's lifetime — e.g. for the lifetime of a Scope or a returned Stream.
Use case
An HTTP route serves attachment bytes as a stream. Downloads must be capped per isolate, and a public URL must not build an unbounded waiter queue — an exhausted pool should answer 503 immediately (fail-fast). The permit has to be held until the returned stream completes, long after the opening effect has returned.
With today's API this requires keeping a child fiber alive as the permit owner inside withPermitsIfAvailable, coordinated with the caller through deferreds — ~70 lines of choreography for "take a permit if free, release it when the scope closes":
const holdPermit = pool.withPermitsIfAvailable(1)(
open.pipe(
Effect.flatMap(stream => Deferred.succeed(ready, wire(stream))),
Effect.andThen(Effect.never), // fiber stays alive as the permit owner
),
)
// + interruption wiring so closing the stream's scope releases the permit
Proposal
/** Acquires the permits only if immediately available; pairs with `release`. */
export const takeIfAvailable: {
(permits: number): (self: Semaphore) => Effect.Effect<boolean>
(self: Semaphore, permits: number): Effect.Effect<boolean>
}
Implementation mirrors the first branch of withPermitsIfAvailable: suspend, free < n → false, else taken += n → true.
With that, scope-owned fail-fast holding is ordinary user code:
const acquired = yield* Semaphore.takeIfAvailable(pool, 1)
if (!acquired) return yield* Effect.fail(new PoolExhausted())
yield* Scope.addFinalizer(scope, Semaphore.release(pool, 1))
Optionally, withPermitsScoped / withPermitsScopedIfAvailable sugar could sit on top, but takeIfAvailable alone closes the gap.
Happy to send a PR with tests and docs if the addition is welcome.
🤖 Drafted with Claude Code
Problem
Semaphore(v4) offers two acquisition styles:take— manual, pairs withrelease, but waits for permitswithPermitsIfAvailable— fail-fast, but brackets a single Effect: the permit is released when that effect exitsThere is no way to acquire a permit fail-fast and keep holding it beyond one effect's lifetime — e.g. for the lifetime of a
Scopeor a returnedStream.Use case
An HTTP route serves attachment bytes as a stream. Downloads must be capped per isolate, and a public URL must not build an unbounded waiter queue — an exhausted pool should answer 503 immediately (fail-fast). The permit has to be held until the returned stream completes, long after the opening effect has returned.
With today's API this requires keeping a child fiber alive as the permit owner inside
withPermitsIfAvailable, coordinated with the caller through deferreds — ~70 lines of choreography for "take a permit if free, release it when the scope closes":Proposal
Implementation mirrors the first branch of
withPermitsIfAvailable: suspend,free < n→false, elsetaken += n→true.With that, scope-owned fail-fast holding is ordinary user code:
Optionally,
withPermitsScoped/withPermitsScopedIfAvailablesugar could sit on top, buttakeIfAvailablealone closes the gap.Happy to send a PR with tests and docs if the addition is welcome.
🤖 Drafted with Claude Code