-- SPDX-FileCopyrightText: 2026 Alexandra de Wit -- -- SPDX-License-Identifier: MIT {- | Async-safe release for a claimed in-flight slot. The metadata cache ("Ecluse.Core.Server.Cache.Store") and the credential refresher ("Ecluse.Core.Credential.Refresh.Internal") each collapse duplicate work onto one execution by claiming a slot. An asynchronous exception taken between the claim and the run that would free it wedges the slot: a follower parks forever and every caller behind it stalls until the process restarts. Both face that hazard, so the release discipline lives here once. -} module Ecluse.Core.InFlight ( guardInFlight, ) where import UnliftIO.Exception (finally, withException) {- | Run a leader's @body@ and release its already-claimed in-flight slot on every exit. Call it inside the mask that committed the claim, pass that mask's @restore@, and leave nothing interruptible between. -} guardInFlight :: -- | The enclosing mask's @restore@, applied to the body so it stays interruptible. (IO a -> IO a) -> {- | Runs with the orphaning failure just before the release, to hand it to a waiting follower. A consumer whose waiters re-decide against the freed slot passes a no-op. -} (SomeException -> IO ()) -> -- | Free the claimed slot. Runs on every exit, asynchronous exceptions included. IO () -> -- | The leader's run, executed under @restore@. IO a -> IO a guardInFlight :: forall a. (IO a -> IO a) -> (SomeException -> IO ()) -> IO () -> IO a -> IO a guardInFlight IO a -> IO a restore SomeException -> IO () onOrphan IO () releaseSlot IO a body = (IO a -> IO a restore IO a body IO a -> (SomeException -> IO ()) -> IO a forall (m :: * -> *) e a b. (MonadUnliftIO m, Exception e) => m a -> (e -> m b) -> m a `withException` SomeException -> IO () onOrphan) IO a -> IO () -> IO a forall (m :: * -> *) a b. MonadUnliftIO m => m a -> m b -> m a `finally` IO () releaseSlot