I am writing a web server using Scotty. The server should have a login route that once user logs in, a token is dispatched and recorded in the server. Obviously the recorded tokens should be in a global state (not using Redis kind of thing because the server is run on single machine and has small view counts).
Now I know the Scotty examples have shown how to define and access global states in routes, but I can't find out how to do the same thing for middlewares.
I tried using the same way from the official example but neither the middleware function nor the app function has a WebM monad context so I can't really do this:
app = do
tokensList <- webM $ gets tokens
middleware $ authMiddleware tokensList
That "official example" isn't so great. I mean, it's a good example of using a custom monad, but it's a lousy example of how to provide global state. You don't need the custom monad at all. You just need the TVar
.
Ultimately, all you need to do is create the newTVar
in main
, and then pass it to the app
function to construct the application and middleware, which can access the TVar
directly with lifted IO
operations in the application and IO
operations in the middleware. It'll look something like the following:
main :: IO ()
main = do
state <- newTVar startState
scotty 3000 $ app state
app :: TVar AppState -> ScottyM ()
app state = do
middleware (authMiddleware state)
get "..." $ do
x <- liftIO $ readTVarIO state
...
authMiddleware :: TVar State -> MiddleWare
authMiddleware state app req resp = do
x <- readTVarIO state
...
app req resp
To illustrate, here's a rewrite of that official example without the monad that operates on the TVar
directly in the handler. (No middleware yet, but see the example further below.)
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Web.Scotty
import Control.Concurrent.STM
import Control.Monad.IO.Class
import Data.String
newtype AppState = AppState { tickCount :: Int }
main :: IO ()
main = do
state <- newTVarIO (AppState 0)
scotty 3000 $ app state
app :: TVar AppState -> ScottyM ()
app state = do
get "/" $ do
c <- liftIO $ tickCount <$> readTVarIO state
text $ fromString $ show c
get "/plusone" $ do
liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+1))
redirect "/"
get "/plustwo" $ do
liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+2))
redirect "/"
and here's a version that uses middleware to count page accesses and queries and resets the value from the handlers, all directly through the TVar
without requiring any custom monad:
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Web.Scotty
import Control.Concurrent.STM
import Control.Monad.IO.Class
import Data.String
import Network.Wai
newtype AppState = AppState { tickCount :: Int }
main = do
state <- newTVarIO (AppState 0)
scotty 3000 $ app state
app :: TVar AppState -> ScottyM ()
app state = do
middleware $ countMiddleware state
get "/" $ do
c <- liftIO $ tickCount <$> readTVarIO state
text $ fromString $ show c
get "/reset" $ do
liftIO . atomically $ writeTVar state (AppState 0)
redirect "/"
countMiddleware :: TVar AppState -> Middleware
countMiddleware state appl req resp = do
liftIO . atomically $ modifyTVar' state (\(AppState n) -> AppState (n+1))
appl req resp