packages feed

servant-subscriber-0.2.0.0: src/Servant/Subscriber/Client.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-}
module Servant.Subscriber.Client where


import           Control.Concurrent.Async
import           Control.Concurrent.STM (STM, atomically, retry)
import           Control.Concurrent.STM.TVar
import           Control.Exception (displayException, SomeException)
import           Control.Exception.Lifted (finally, try)
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Logger (MonadLogger, logDebug, logError, logInfo, monadLoggerLog)
import           Control.Monad.Trans.Control (MonadBaseControl)
import           Data.Aeson
import           Data.Map (Map)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import qualified Network.WebSockets as WS
import           Network.WebSockets.Connection as WS

import           Servant.Subscriber.Backend
import           Servant.Subscriber.Request
import           Servant.Subscriber.Response as Resp
import           Servant.Subscriber.Types as S

type ClientMonitors = Map Path StatusMonitor

data Client = Client {
    monitors      :: !(TVar ClientMonitors)
  , readRequest   :: !(IO (Maybe Request))
  , writeResponse :: !(Response -> IO ())
  }

data StatusMonitor = StatusMonitor {
  request   :: !HttpRequest
, monitor   :: !(TVar (RefCounted ResourceStatus))
, oldStatus :: !ResourceStatus
}

data Snapshot = Snapshot {
  snapshotCurrent :: ResourceStatus
, fullMonitor     :: StatusMonitor
}

snapshotOld :: Snapshot -> ResourceStatus
snapshotOld = oldStatus . fullMonitor

toSnapshot :: StatusMonitor -> STM Snapshot
toSnapshot mon = do
  current <- readTVar $ monitor mon
  return Snapshot {
    snapshotCurrent = refValue current
  , fullMonitor = mon
  }



snapshotRequest :: Snapshot -> HttpRequest
snapshotRequest = request . fullMonitor

fromWebSocket :: WS.Connection -> STM Client
fromWebSocket c = do
  ms <- newTVar Map.empty
  return Client {
    monitors = ms
  , readRequest = do
      msg <- WS.receiveDataMessage c
      case msg of
        WS.Text bs  -> return $ decode bs
        WS.Binary _ -> error "Sorry - binary connections currently unsupported!"
  , writeResponse = sendDataMessage c . WS.Text . encode
  }

run :: (MonadLogger m, MonadBaseControl IO m, MonadIO m, Backend backend) => backend -> Subscriber api -> Client -> m ()
run b sub c = do
  let
    work    = liftIO $ race_ (runMonitor b c) (handleRequests b sub c)
    cleanup = liftIO . atomically $ do
      ms <- readTVar (monitors c)
      mapM_ (unsubscribeMonitor sub) ms
  r <- try $ finally work cleanup
  case r of
    Left e -> $logDebug $ T.pack $ displayException (e :: SomeException)
    Right _ -> return ()

unsubscribeMonitor :: Subscriber api -> StatusMonitor -> STM ()
unsubscribeMonitor sub m =
  let
    path = httpPath . request $ m
    mon = monitor m
  in
    unsubscribe path mon sub

subscribeMonitor :: Subscriber api -> HttpRequest -> Client -> STM ()
subscribeMonitor sub req c = do
  let path = httpPath req
  tState <- subscribe path sub
  stateVal <- refValue <$> readTVar tState
  modifyTVar' (monitors c) $ Map.insert path (StatusMonitor req tState stateVal)

handleRequests :: Backend backend => backend -> Subscriber api -> Client -> IO ()
handleRequests b sub c = forever $ do
    req <- readRequest c
    case req of
      Nothing                 -> writeResponse c (RequestError ParseError)
      Just (Subscribe req)    -> handleSubscribe b sub c req
      Just (Unsubscribe path) -> handleUnsubscribe b sub c path

handleSubscribe :: Backend backend => backend -> Subscriber api -> Client -> HttpRequest -> IO ()
handleSubscribe b sub c req = void $ requestResource b req $ \ httpResponse -> do
    let status = statusCode . httpStatus $ httpResponse
    let path = httpPath req
    let isGoodStatus = status >= 200 && status < 300 -- For now we only accept success
    if isGoodStatus
      then
        mapM_ (writeResponse c) <=< atomically $ do
          ms <- readTVar (monitors c)
          case Map.lookup path ms of
            Just _  -> return [RequestError (AlreadySubscribed path)]
            Nothing -> do
              subscribeMonitor sub req c
              return [Resp.Subscribed path, Resp.Modified path httpResponse]
      else
        writeResponse c $ RequestError (HttpRequestFailed req httpResponse)
    return ResponseReceived

handleUnsubscribe :: Backend backend => backend -> Subscriber api -> Client -> Path -> IO ()
handleUnsubscribe b sub c path = writeResponse c <=< atomically $ do
    ms <- readTVar (monitors c)
    case Map.lookup path ms of
      Nothing -> return $ RequestError (NoSuchSubscription path)
      Just m -> do
        unsubscribeMonitor sub m
        modifyTVar (monitors c) $ Map.delete path
        return $ Unsubscribed path

runMonitor :: Backend backend => backend -> Client -> IO ()
runMonitor b c = forever $ do
    changes <- atomically $ monitorChanges c
    mapM_ (sendUpdate b (writeResponse c)) changes

sendUpdate :: Backend backend => backend -> (Response -> IO ()) -> (HttpRequest, ResourceStatus) -> IO ()
sendUpdate b sendResponse (req, S.Deleted)    = sendResponse $ Resp.Deleted (httpPath req)
sendUpdate b sendResponse (req, S.Modified _) = sendServerResponse b req sendResponse

sendServerResponse :: Backend backend => backend -> HttpRequest -> (Response -> IO ()) -> IO ()
sendServerResponse b req sendResponse = void $ requestResource b req
  $ \ httpResponse -> do
    let path = httpPath req
    sendResponse $ Resp.Modified path httpResponse
    return ResponseReceived

monitorChanges :: Client -> STM [(HttpRequest, ResourceStatus)]
monitorChanges c = do
      snapshots <- mapM toSnapshot . Map.elems =<< readTVar (monitors c)
      let result = getChanges snapshots
      if null result
        then retry
        else do
          let newMonitors = monitorsFromList . updateMonitors $ snapshots
          writeTVar (monitors c) newMonitors
          return result


getChanges :: [Snapshot] -> [(HttpRequest, ResourceStatus)]
getChanges = map toChangeReport . filter monitorChanged

monitorChanged :: Snapshot -> Bool
monitorChanged m = snapshotCurrent m /= snapshotOld m

toChangeReport :: Snapshot -> (HttpRequest, ResourceStatus)
toChangeReport m = (snapshotRequest m, snapshotCurrent m)

updateMonitors :: [Snapshot] -> [StatusMonitor]
updateMonitors = map updateOldStatus . filter ((/= S.Deleted) . snapshotCurrent)

updateOldStatus :: Snapshot -> StatusMonitor
updateOldStatus m = (fullMonitor m) {
    oldStatus = snapshotCurrent m
  }

monitorsFromList :: [StatusMonitor] -> ClientMonitors
monitorsFromList ms = let
    paths = map (httpPath . request) ms
    assList = zip paths ms
  in
    Map.fromList assList