packages feed

mu-graphql-0.3.0.0: src/Mu/GraphQL/Subscription/Protocol.hs

{-# language FlexibleContexts    #-}
{-# language OverloadedStrings   #-}
{-# language ScopedTypeVariables #-}
{-
This module implements the protocol as specified in
https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md
-}
module Mu.GraphQL.Subscription.Protocol where

import           Control.Applicative
import           Control.Concurrent
import           Control.Concurrent.Async
import           Control.Concurrent.STM
import           Control.Monad                 (forM_)
import           Control.Monad.IO.Class        (MonadIO (liftIO))
import           Data.Aeson                    ((.:), (.:?), (.=))
import qualified Data.Aeson                    as A
import           Data.Conduit
import qualified Data.HashMap.Strict           as HM
import qualified Data.Text                     as T
import           Language.GraphQL.Draft.Parser (parseExecutableDoc)
import           Language.GraphQL.Draft.Syntax (ExecutableDocument)
import qualified ListT                         as L
import           Network.WebSockets
import qualified StmContainers.Map             as M

import           Mu.GraphQL.Query.Parse

protocol :: ( Maybe T.Text -> VariableMapC -> ExecutableDocument
              -> ConduitT A.Value Void IO ()
              -> IO () )
         -> Connection -> IO ()
protocol f conn = start
  where
    -- listen for GQL_CONNECTION_INIT
    start = do
      msg <- receiveJSON conn
      case msg of
        Just (GQLConnectionInit _)
          -> do -- send GQL_CONNECTION_ACK
                sendJSON conn GQLConnectionAck
                vars <- M.newIO
                -- send GQL_KEEP_ALIVE each 1s.
                withAsync keepAlive $ \ka ->
                -- start listening for incoming messages
                  listen ka vars
        _ -> start  -- Keep waiting
    -- keep-alive
    keepAlive = do
      sendJSON conn GQLKeepAlive
      threadDelay 1000000
      keepAlive
    -- listen for messages from client
    listen ka vars = do
      msg <- receiveJSON conn
      case msg of
        Just (GQLStart i q v o)  -- start handling
          -> withAsync (handle i q v o >> atomically (M.delete i vars)) $ \t -> do
             atomically $ M.insert t i vars
             listen ka vars
        Just (GQLStop i)  -- stop with handling that query
          -> do r <- atomically $ M.lookup i vars
                case r of
                  Nothing -> return ()
                  Just a  -> do cancel a
                                atomically $ M.delete i vars
                listen ka vars
        Just GQLTerminate  -- terminate all queries
          -> do cancelAll ka vars
                sendClose conn ("GraphQL session terminated" :: T.Text)
        _ -> listen ka vars  -- Keep going
    -- Handle a single query
    handle i q v o
      = case parseExecutableDoc q of
          Left err -> sendJSON conn (GQLError i (A.toJSON err))
          Right d  -> do
            f o v d (cndt i)
            sendJSON conn (GQLComplete i)
    -- Conduit which sends the results via the wire
    cndt i = do
      msg <- await
      case msg of
        Nothing -> return ()
        Just v  -> do liftIO $ sendJSON conn (GQLData i v)
                      cndt i
    -- Cancel all pending subscriptions
    cancelAll ka vars
      = do cancel ka
           vs <- atomically $ L.toList $ M.listT vars
           forM_ (map snd vs) cancel

receiveJSON :: A.FromJSON a => Connection -> IO (Maybe a)
receiveJSON conn = do
  d <- receiveData conn
  return $ A.decode d

sendJSON :: A.ToJSON a => Connection -> a -> IO ()
sendJSON conn v
  = sendTextData conn (A.encode v)

data ClientMessage
  = GQLConnectionInit { initPayload :: Maybe A.Value }
  | GQLStart { clientMsgId   :: T.Text
             , query         :: T.Text
             , variables     :: VariableMapC
             , operationName :: Maybe T.Text}
  | GQLStop { clientMsgId :: T.Text }
  | GQLTerminate
  deriving Show

data ServerMessage
  = GQLConnectionError { errorPayload :: Maybe A.Value }
  | GQLConnectionAck
  | GQLData     { serverMsgId :: T.Text
                , payload     :: A.Value }
  | GQLError    { serverMsgId :: T.Text
                , payload     :: A.Value }
  | GQLComplete { serverMsgId :: T.Text}
  | GQLKeepAlive
  deriving Show

instance A.FromJSON ClientMessage where
  parseJSON = A.withObject "ClientMessage" $ \v -> do
     ty :: String <- v .: "type"
     case ty of
       "connection_init"
         -> GQLConnectionInit <$> v .:? "payload"
       "start"
         -> do i <- v .: "id"
               (q,vrs,opN) <- v .: "payload" >>= parsePayload
               pure $ GQLStart i q vrs opN
       "stop"
         -> GQLStop <$> v .: "id"
       "terminate"
         -> pure GQLTerminate
       _ -> empty
    where
      parsePayload = A.withObject "ClientMessage/GQL_START" $
        \v -> (,,) <$> v .: "query"
                   <*> (v .: "variables" <|> pure HM.empty)
                   <*> v .:? "operationName"

theType :: (A.KeyValue kv) => T.Text -> kv
theType t = "type" .= t

instance A.ToJSON ServerMessage where
  toJSON (GQLConnectionError e)
    = A.object [theType "connection_error", "payload" .= e]
  toJSON GQLConnectionAck
    = A.object [theType "connection_acl"]
  toJSON (GQLData i p)
    = A.object [theType "data", "id" .= i, "payload" .= p]
  toJSON (GQLError i p)
    = A.object [theType "error", "id" .= i, "payload" .= p]
  toJSON (GQLComplete i)
    = A.object [theType "complete", "id" .= i]
  toJSON GQLKeepAlive
    = A.object [theType "connection_keep_alive"]