packages feed

snap-extras-0.12.3.2: poll-example/PollExample.hs

{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE RecordWildCards           #-}
{-# LANGUAGE TemplateHaskell           #-}
module Main where

------------------------------------------------------------------------------
import           Control.Applicative
import           Control.Concurrent
import           Control.Concurrent.MVar
import           Control.Lens
import           Control.Monad
import           Control.Monad.State
import           Control.Monad.Trans
import           Data.Aeson
import           Data.IORef
import qualified Data.Map                    as M
import qualified Data.Map.Syntax             as MS
import           Data.Monoid
import           Data.Readable
import qualified Data.Text                   as T
import qualified Data.Text.Encoding          as T
import           Data.Time
import           Heist
import           Heist.Compiled
import           Snap.Core
import           Snap.Http.Server
import           Snap.Snaplet
import           Snap.Snaplet.Heist.Compiled
import           Snap.Util.FileServe
------------------------------------------------------------------------------
import           Snap.Extras.CoreUtils
import           Snap.Extras.PollStatus
------------------------------------------------------------------------------

------------------------------------------------------------------------------
--         Job infrastructure that would have been necessary anyway
------------------------------------------------------------------------------


data JobRepo = JobRepo
    { repoNextInd :: Int
    , repoJobs    :: M.Map Int Status
    }

emptyJobRepo = JobRepo 0 M.empty

newJob :: UTCTime -> UTCTime -> JobRepo -> (JobRepo, Int)
newJob ts curTs repo = (JobRepo (i+1) jobs, i)
  where
    i = repoNextInd repo
    jobs = M.insert i (Status (Just ts) curTs Running [] 0 10) $
             repoJobs repo

updateJob :: Int -> (Status -> Status) -> JobRepo -> (JobRepo, ())
updateJob jobId f repo = (JobRepo (repoNextInd repo) newJobs, ())
  where
    curJobs = repoJobs repo
    newJobs = M.update (g . f) jobId curJobs
    g s = if statusAmountCompleted s == statusAmountTotal s
            then Just $ s { statusJobState = FinishedSuccess }
            else Just s

jobAction
    :: IORef JobRepo
    -> Int
    -> Double
    -> IO ()
jobAction ref jobId seconds = do
    let inc = seconds / 100.0
        numIters = ceiling $ seconds / inc
    forM_ [1..numIters] $ \n -> do
        threadDelay $ round $ inc * 1000000
        ts <- liftIO getCurrentTime
        let setStatus s = s { statusTimestamp = ts
                            , statusAmountCompleted = (fromIntegral n * inc) }
        atomicModifyIORef' ref (updateJob jobId setStatus)


------------------------------------------------------------------------------
--                    Web app boilerplate for this test
------------------------------------------------------------------------------


data App = App
    { _heist :: Snaplet (Heist App)
    , _repo  :: IORef JobRepo
    }
makeLenses ''App

instance HasHeist App where
    heistLens = subSnaplet heist

routes = [ ("startJob", startlongjob)
         , ("myJobStatus", jobStatusHandler "_status" ".statusdiv")
         , ("", heistServe)
         , ("", serveDirectory "static")
         ]

appInit = makeSnaplet "app" "blah" Nothing $ do
    h <- nestSnaplet "" heist $ heistInit' "" hc
    addRoutes routes
    ref <- liftIO $ newIORef emptyJobRepo
    return $ App h ref
  where
    hc = emptyHeistConfig
      & hcLoadTimeSplices .~ defaultLoadTimeSplices
      & hcCompiledSplices .~ splices

main :: IO ()
main = do
    serveSnaplet defaultConfig appInit


------------------------------------------------------------------------------
--                 Core code needed to create a polling job
------------------------------------------------------------------------------


------------------------------------------------------------------------------
-- | You have to define at least these three splices in your application.  The
-- first two are static.  You'll need an instance of the third one for each
-- type of job you need status for.
splices = do
    -- You need one of these status splices per status job type
    "jobStatus" MS.## statusSplice statusSplices getUrl getMyJobStatus
                                   statusFinished
  where
    getUrl = do
        jobId <- getParam "jobId"
        return $ fmap (T.decodeUtf8 . ("myJobStatus?jobId=" <>)) jobId


------------------------------------------------------------------------------
-- | Starts a job and returns its job ID.
startlongjob :: Handler App App ()
startlongjob = do
    ref <- gets _repo
    ts <- liftIO getCurrentTime
    jobId <- liftIO $ atomicModifyIORef' ref (newJob ts ts)
    liftIO $ forkIO $ jobAction ref jobId 10
    redirect $ T.encodeUtf8 $ "jobstatus?jobId=" <> (T.pack $ show jobId)


------------------------------------------------------------------------------
-- | A function that gets the status of a job.
getMyJobStatus :: Handler App App (Maybe Status)
getMyJobStatus = do
    ref <- gets _repo
    repo <- liftIO $ readIORef ref
    mjidbs <- getParam "jobId"
    return $ do
        jobId <- fromBS =<< mjidbs
        M.lookup jobId (repoJobs repo)