Last step – evaluate in 2 different monad contexts
Once the app is initialized, which includes setting the amazonka env
, there are two phases to serving each request.
- Receive the user-request (requires the servant handler context)
- Update the in-memory data using data streamed from the S3 object store (does not require the servant handler)
- Instantiate and serve the graphql request (servant handler)
I suspect the final answer gets the following to work.
(I get the code to compile with hollow instances of MonadUnliftIO
and MonadResource
for the servant Handler
).
-- AppObs is a custom monad that wraps the servant handler
-- => a single monad that needs to be "partially" evaluated?? OR
-- => temporarily augment the monad is a nested do-block
--
-- 🔖 The system relies on the sequence that I won't know "is working"
-- until I get the code to run (i.e., the request must only be executed
-- once the db has been updated... with a stream... using conduit)
--
api :: ProjectId -> GQLRequest -> AppObs GQLResponse
api pid req = do
-- run a side-effect outside the custom Servant Handler
_ <- setDbWithS3 pid
interpreter gqlRoot req
Streaming the getObject
operation to mutate the in-memory db
-- |
-- Update database with ObsEtlInput retrieved from S3
--
setDbWithS3 :: (MonadCatch m, MonadLogger m, MonadReader Env m, MonadResource m)
=> ProjectId -> m (ConduitT () S3.ByteString (ResourceT IO) ())
setDbWithS3 pid = do
... -- access the app env (db mutable ref, cfg and s3env)
resourceState <- createInternalState
obsEtl <- flip runInternalState resourceState $ do -- do :: ResourceT m ObsETL
awsRes <- request pid cfg s3env'
rawBytes <- S3.body awsRes `S3.sinkBody` CB.sinkLbs -- ConduitM ByteString Void (ResourceT IO) a
tryObsFromResponse rawBytes
-- side-effect: mutate db state
let newStore = dbNew pid obsEtl
liftIO . atomically $ App.writeTVar dbTVar newStore
pure $ closeInternalState resourceState -- this may not guarantee seq
The custom monad and natural transformation
-- not able to host ResourceT?, certainly not UnliftIO
newtype AppObs a =
AppObs
{ iniApp :: ReaderT Env (LoggingT Handler) a
}
deriving newtype ( Functor ... )
-- custom monad -> servant handler
nat :: Env -> AppObs a -> Handler a
nat env app = runStderrLoggingT (
runReaderT (iniApp app) env
)
--
-- Where the use servant's `enter` might come into play
-- in the 2-context strategy
--
-- serve :: Proxy Api -> (ServerT api Handler) -> Application
-- hoistServer :: HasServer api '[]
-- => Proxy api -> (forall x. m x -> n x)
-- -> ServerT api m -> ServerT api n
--
-- 🔖 Requires Servant.Conduit to access ToSourceIO and FromSourceIO
--
app :: Env -> Application
app env = logStdoutDev . cors ( const $ Just corsPolicy )
. serve apiType $ hoistServer apiType (nat env) appM
This is new territory for me…
I hope that I’m articulating the strategy in a way that makes sense. Am I framing the problem in a way that taps into an idiomatic Haskell train of thought and approach?