packages feed

cachix-api-1.6.1: src/Cachix/API/WebSocketSubprotocol.hs

{-# LANGUAGE DuplicateRecordFields #-}

module Cachix.API.WebSocketSubprotocol where

import qualified Control.Concurrent.Async as Async
import qualified Control.Concurrent.STM.TMQueue as TMQueue
import qualified Control.Exception.Safe as Safe
import qualified Data.Aeson as Aeson
import Data.Time (UTCTime)
import Data.UUID (UUID)
import qualified Network.WebSockets as WS
import Protolude

data Message cmd = Message
  { method :: Text,
    command :: cmd,
    agent :: Maybe UUID,
    id :: UUID
  }
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

data Cache = Cache
  { cacheName :: Text,
    publicKey :: Text,
    isPublic :: Bool
  }
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

data AgentInformation = AgentInformation
  { cache :: Maybe Cache,
    id :: UUID
  }
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

data DeploymentDetails = DeploymentDetails
  { storePath :: Text,
    id :: UUID,
    index :: Int64,
    rollbackScript :: Maybe Text
  }
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

data BackendCommand
  = Deployment DeploymentDetails
  | AgentRegistered AgentInformation
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

data AgentCommand
  = DeploymentStarted {id :: UUID, time :: UTCTime, closureSize :: Maybe Int64}
  | DeploymentFinished {id :: UUID, time :: UTCTime, hasSucceeded :: Bool}
  deriving (Show, Eq, Generic, Aeson.FromJSON, Aeson.ToJSON)

parseMessage :: (Aeson.FromJSON cmd) => ByteString -> Either Text (Message cmd)
parseMessage = first toS . Aeson.eitherDecodeStrict'

sendMessage :: (Aeson.ToJSON cmd) => WS.Connection -> Message cmd -> IO ()
sendMessage connection cmd =
  WS.sendTextData connection (Aeson.encode cmd)

-- | Receive and process messages in parallel.
--
-- Note: This will not rethrow the 'CloseRequest' exception!
--
-- TODO: use Async.replicateConcurrently
receiveDataConcurrently :: WS.Connection -> (ByteString -> IO ()) -> IO ()
receiveDataConcurrently connection action =
  do
    queue <- atomically TMQueue.newTMQueue
    Async.withAsync (consumer queue) (producer queue)
  where
    producer queue consumerThread =
      loop
        `finally` closeGracefully queue consumerThread
        `Safe.catch` closeRequest
      where
        loop = do
          payload <- WS.receiveData connection
          atomically $ TMQueue.writeTMQueue queue payload
          loop

    consumer queue = loop
      where
        loop = do
          payload <- atomically $ TMQueue.readTMQueue queue
          case payload of
            Nothing -> return ()
            Just msg -> action msg *> loop

    -- Close the queue and wait for the consumer finish processing messages.
    closeGracefully :: TMQueue.TMQueue a -> Async.Async () -> IO ()
    closeGracefully queue consumerThread = do
      atomically $ TMQueue.closeTMQueue queue
      Async.wait consumerThread

    closeRequest :: WS.ConnectionException -> IO ()
    closeRequest (WS.CloseRequest _ _) = return ()
    closeRequest e = throwIO e

data Log = Log
  { line :: Text,
    time :: UTCTime
  }
  deriving (Generic, Show, Aeson.ToJSON, Aeson.FromJSON)