packages feed

om-socket 0.11.0.5 → 1.0.0.0

raw patch · 12 files changed

+667/−495 lines, 12 filesdep +hspecdep +om-forkdep +streamingdep −binary-conduitdep −conduitdep −conduit-extraPVP ok

version bump matches the API change (PVP)

Dependencies added: hspec, om-fork, streaming, streaming-binary, streaming-bytestring, unliftio-core

Dependencies removed: binary-conduit, conduit, conduit-extra, time, transformers

API changes (from Hackage documentation)

- OM.Socket: connectServer :: (Binary request, Binary response, MonadIO m, MonadLoggerIO n, Show response) => AddressDescription -> Maybe ClientParams -> n (request -> m response)
+ OM.Socket: connectServer :: forall n request m response. (Binary request, Binary response, MonadIO m, MonadLoggerIO n, Show response) => AddressDescription -> Maybe ClientParams -> n (request -> m response)
- OM.Socket: openEgress :: forall o (m :: Type -> Type). (Binary o, MonadFail m, MonadIO m, MonadThrow m) => AddressDescription -> ConduitT o Void m ()
+ OM.Socket: openEgress :: (Binary o, MonadFail m, MonadIO m) => AddressDescription -> Stream (Of o) m r -> m r
- OM.Socket: openIngress :: forall i (m :: Type -> Type). (Binary i, MonadIO m, MonadFail m) => AddressDescription -> ConduitT () i m ()
+ OM.Socket: openIngress :: forall i (m :: Type -> Type) never_returns. (Binary i, MonadFail m, MonadIO m, Race) => AddressDescription -> Stream (Of i) m never_returns
- OM.Socket: openServer :: (Binary request, Binary response, MonadFail m, MonadLoggerIO m, Show request, Show response) => AddressDescription -> Maybe (IO ServerParams) -> ConduitT Void (request, response -> m Responded) m ()
+ OM.Socket: openServer :: (Binary request, Binary response, MonadLogger m, MonadCatch m, MonadFail m, MonadUnliftIO m, Race) => AddressDescription -> Maybe (IO ServerParams) -> Stream (Of (request, response -> m Responded)) m never_returns

Files

README.md view
@@ -40,30 +40,40 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE OverloadedStrings #-} -module Main (main) where+module Ingress (main) where -import Conduit ((.|), awaitForever, runConduit) import Control.Monad.IO.Class (MonadIO(liftIO)) import Data.Binary (Binary)+import Data.Function ((&)) import GHC.Generics (Generic)+import OM.Fork (runRace) import OM.Socket (openIngress)+import Prelude (($), IO, putStrLn)+import qualified Streaming.Prelude as Stream -{- | The messages that arrive on the socket. -}+{- |+  The messages that arrive on the socket.++  This type would typically be shared by both the Ingress and Egress side,+  or at least the binary encoding must be identical.+-} data Msg   = A   | B   deriving stock (Generic)   deriving anyclass (Binary) + main :: IO () main =-  runConduit $+  runRace $     openIngress "localhost:9000"-    .| awaitForever (\msg ->-         case msg of-           A -> liftIO $ putStrLn "Got A"-           B -> liftIO $ putStrLn "Got B"-       )+    & Stream.mapM_+       (\msg ->+           case msg of+             A -> liftIO $ putStrLn "Got A"+             B -> liftIO $ putStrLn "Got B"+         ) ```    @@ -75,25 +85,33 @@ {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE OverloadedStrings #-} -module Main (main) where+module Egress (main) where -import Conduit ((.|), runConduit, yield) import Data.Binary (Binary)+import Data.Function ((&)) import GHC.Generics (Generic) import OM.Socket (openEgress)+import Prelude (IO)+import qualified Streaming.Prelude as Stream -{- | The messages that arrive on the socket. -}+{- |+  The messages that arrive on the socket.++  This type would typically be shared between the client and the server,+  or at least the binary representation must be identical (as, for+  example, if the client is not written in Haskell).+-} data Msg   = A   | B   deriving stock (Generic)   deriving anyclass (Binary) + main :: IO () main =-  runConduit $-    mapM_ yield [A, B, B, A, A, A, B]-    .| openEgress "localhost:9000"+  Stream.each [A, B, B, A, A, A, B]+  & openEgress "localhost:9000"  ``` @@ -105,39 +123,55 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} -module Main (main) where -import Conduit ((.|), awaitForever, runConduit)+module Server (main) where+ import Control.Monad.Logger (runStdoutLoggingT)-import Control.Monad.Trans.Class (MonadTrans(lift)) import Data.Binary (Binary)+import Data.Function ((&))+import OM.Fork (runRace) import OM.Socket (openServer)+import Prelude (Maybe(Nothing), ($), IO, Show, String)+import qualified Streaming.Prelude as Stream -{- | The requests accepted by the server. -}++{-|+  The requests accepted by the server.++  This type would typically be shared between client and server.+-} newtype Request = EchoRequest String   deriving newtype (Binary, Show)  -{- | The response sent back to the client. -}+{-|+  The response sent back to the client.++  This type would typically be shared between client and server.+-} newtype Responsee = EchoResponse String   deriving newtype (Binary, Show)  -{- | Simple echo resposne server. -}+{-| Simple echo resposne server. -} main :: IO () main =-  runStdoutLoggingT . runConduit $-    pure ()-    .| openServer "localhost:9000" Nothing-    .| awaitForever (\(EchoRequest str, respond) ->-        {--          You don't necessarily have to respond right away if you don't-          want to. You can cache the responder away in some state and-          get back to it at some later time if you like.-        -}-        lift $ respond (EchoResponse str)+  runRace+  $ runStdoutLoggingT+  $ (+      openServer "localhost:9000" Nothing+      & Stream.mapM_+          (\ (EchoRequest str, respond) ->+            {-+              You don't necessarily have to respond right away if+              you don't want to. You can cache the responder away in+              some state and get back to it at some later time if you+              like. `openServer` and `connectServer` have a mechnamism+              for handling out of order responses.+            -}+            respond (EchoResponse str)+          )     )- ```  ### Connect a client to a server@@ -147,23 +181,32 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} -module Main (main) where+module Client (main) where  import Control.Monad.Logger (runStdoutLoggingT) import Data.Binary (Binary) import OM.Socket (connectServer) -{- | The requests accepted by the server. -}+{-|+  The requests accepted by the server.++  This would typically need to be shared between the client and the+  server. Certainly the binary encoding must be identical.+-} newtype Request = EchoRequest String   deriving newtype (Binary, Show)  -{- | The response sent back to the client. -}+{-|+  The response sent back to the client.++  Likewise, this would be shared between client and server.+-} newtype Responsee = EchoResponse String   deriving newtype (Binary, Show)  -{- | Simple echo resposne client. -}+{-| Simple echo resposne client. -} main :: IO () main = do   client <-
+ examples/Client.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}++module Client (main) where++import Control.Monad.Logger (runStdoutLoggingT)+import Data.Binary (Binary)+import OM.Socket (connectServer)++{-|+  The requests accepted by the server.++  This would typically need to be shared between the client and the+  server. Certainly the binary encoding must be identical.+-}+newtype Request = EchoRequest String+  deriving newtype (Binary, Show)+++{-|+  The response sent back to the client.++  Likewise, this would be shared between client and server.+-}+newtype Responsee = EchoResponse String+  deriving newtype (Binary, Show)+++{-| Simple echo resposne client. -}+main :: IO ()+main = do+  client <-+    runStdoutLoggingT $+      connectServer "localhost:9000" Nothing+  putStrLn =<< client (EchoRequest "hello")+  putStrLn =<< client (EchoRequest "world")++
+ examples/Egress.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++module Egress (main) where++import Data.Binary (Binary)+import Data.Function ((&))+import GHC.Generics (Generic)+import OM.Socket (openEgress)+import Prelude (IO)+import qualified Streaming.Prelude as Stream++{- |+  The messages that arrive on the socket.++  This type would typically be shared between the client and the server,+  or at least the binary representation must be identical (as, for+  example, if the client is not written in Haskell).+-}+data Msg+  = A+  | B+  deriving stock (Generic)+  deriving anyclass (Binary)+++main :: IO ()+main =+  Stream.each [A, B, B, A, A, A, B]+  & openEgress "localhost:9000"++
+ examples/Ingress.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++module Ingress (main) where++import Control.Monad.IO.Class (MonadIO(liftIO))+import Data.Binary (Binary)+import Data.Function ((&))+import GHC.Generics (Generic)+import OM.Fork (runRace)+import OM.Socket (openIngress)+import Prelude (($), IO, putStrLn)+import qualified Streaming.Prelude as Stream++{- |+  The messages that arrive on the socket.++  This type would typically be shared by both the Ingress and Egress side,+  or at least the binary encoding must be identical.+-}+data Msg+  = A+  | B+  deriving stock (Generic)+  deriving anyclass (Binary)+++main :: IO ()+main =+  runRace $+    openIngress "localhost:9000"+    & Stream.mapM_+       (\msg ->+           case msg of+             A -> liftIO $ putStrLn "Got A"+             B -> liftIO $ putStrLn "Got B"+         )++
+ examples/Server.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+++module Server (main) where++import Control.Monad.Logger (runStdoutLoggingT)+import Data.Binary (Binary)+import Data.Function ((&))+import OM.Fork (runRace)+import OM.Socket (openServer)+import Prelude (Maybe(Nothing), ($), IO, Show, String)+import qualified Streaming.Prelude as Stream+++{-|+  The requests accepted by the server.++  This type would typically be shared between client and server.+-}+newtype Request = EchoRequest String+  deriving newtype (Binary, Show)+++{-|+  The response sent back to the client.++  This type would typically be shared between client and server.+-}+newtype Responsee = EchoResponse String+  deriving newtype (Binary, Show)+++{-| Simple echo resposne server. -}+main :: IO ()+main =+  runRace+  $ runStdoutLoggingT+  $ (+      openServer "localhost:9000" Nothing+      & Stream.mapM_+          (\ (EchoRequest str, respond) ->+            {-+              You don't necessarily have to respond right away if+              you don't want to. You can cache the responder away in+              some state and get back to it at some later time if you+              like. `openServer` and `connectServer` have a mechnamism+              for handling out of order responses.+            -}+            respond (EchoResponse str)+          )+    )++
− examples/client.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}--{-# OPTIONS_GHC -Wwarn #-}--module Main (main) where--import Control.Monad.Logger (runStdoutLoggingT)-import Data.Binary (Binary)-import OM.Socket (connectServer)--{- | The requests accepted by the server. -}-newtype Request = EchoRequest String-  deriving newtype (Binary, Show)---{- | The response sent back to the client. -}-newtype Responsee = EchoResponse String-  deriving newtype (Binary, Show)---{- | Simple echo resposne server. -}-main :: IO ()-main = do-  {--    Don't actually call sendRequests, because there is no server running to-    connect to, which will cause an error, which will cause the test to fail.-  -}-  -- sendRequests-  pure ()---sendRequests :: IO ()-sendRequests = do-  client <--    runStdoutLoggingT $-      connectServer "localhost:9000" Nothing-  putStrLn =<< client (EchoRequest "hello")-  putStrLn =<< client (EchoRequest "world")--
− examples/egress.hs
@@ -1,37 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE OverloadedStrings #-}--{-# OPTIONS_GHC -Wwarn #-}--module Main (main) where--import Conduit ((.|), runConduit, yield)-import Data.Binary (Binary)-import GHC.Generics (Generic)-import OM.Socket (openEgress)--{- | The messages that arrive on the socket. -}-data Msg-  = A-  | B-  deriving stock (Generic)-  deriving anyclass (Binary)--main :: IO ()-main =-  {--    Don't actually call sendMessages, because there is no server running to-    connect to, which will cause an error, which will cause the test to fail.-  -}-  -- sendMessages-  pure ()--sendMessages :: IO ()-sendMessages = do-  runConduit $-    mapM_ yield [A, B, B, A, A, A, B]-    .| openEgress "localhost:9000"--
− examples/ingress.hs
@@ -1,42 +0,0 @@-{-# LANGUAGE DeriveAnyClass #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE OverloadedStrings #-}--{-# OPTIONS_GHC -Wwarn #-}--module Main (main) where--import Conduit ((.|), awaitForever, runConduit)-import Control.Monad.IO.Class (MonadIO(liftIO))-import Data.Binary (Binary)-import GHC.Generics (Generic)-import OM.Socket (openIngress)--{- | The messages that arrive on the socket. -}-data Msg-  = A-  | B-  deriving stock (Generic)-  deriving anyclass (Binary)--main :: IO ()-main =-  {--    Don't actually call serveForever, because the "test" we are using to make-    sure this compiles will never finish running!-  -}-  -- serveForever-  pure ()--serveForever :: IO ()-serveForever =-  runConduit $-    openIngress "localhost:9000"-    .| awaitForever (\msg ->-         case msg of-           A -> liftIO $ putStrLn "Got A"-           B -> liftIO $ putStrLn "Got B"-       )--  
− examples/server.hs
@@ -1,49 +0,0 @@-{-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}--{-# OPTIONS_GHC -Wwarn #-}--module Main (main) where--import Conduit ((.|), awaitForever, runConduit)-import Control.Monad.Logger (runStdoutLoggingT)-import Control.Monad.Trans.Class (MonadTrans(lift))-import Data.Binary (Binary)-import OM.Socket (openServer)--{- | The requests accepted by the server. -}-newtype Request = EchoRequest String-  deriving newtype (Binary, Show)---{- | The response sent back to the client. -}-newtype Responsee = EchoResponse String-  deriving newtype (Binary, Show)---{- | Simple echo resposne server. -}-main :: IO ()-main = do-  {--    Don't actually call server, because the "test" we are using to make-    sure this compiles will never finish running!-  -}-  -- server-  pure ()---server :: IO ()-server =-  runStdoutLoggingT . runConduit $-    pure ()-    .| openServer "localhost:9000" Nothing-    .| awaitForever (\(EchoRequest str, respond) ->-        {--          You don't necessarily have to respond right away if you don't-          want to. You can cache the responder away in some state and-          get back to it at some later time if you like.-        -}-        lift $ respond (EchoResponse str)-    )-
om-socket.cabal view
@@ -1,6 +1,6 @@ cabal-version:       3.0 name:                om-socket-version:             0.11.0.5+version:             1.0.0.0 synopsis:            Socket utilities. description:         Binary ingress server, egress client, and                      bidirectional binarry client/server@@ -25,23 +25,24 @@  common dependencies   build-depends:-    , aeson          >= 2.0.3.0   && < 2.3-    , base           >= 4.15.1.0  && < 4.20-    , binary         >= 0.8.8.0   && < 0.9-    , binary-conduit >= 1.3.1     && < 1.4-    , bytestring     >= 0.10.12.1 && < 0.13-    , conduit        >= 1.3.4.3   && < 1.4-    , conduit-extra  >= 1.3.6     && < 1.4-    , containers     >= 0.6.4.1   && < 0.7-    , exceptions     >= 0.10.4    && < 0.11-    , megaparsec     >= 9.2.2     && < 9.7-    , monad-logger   >= 0.3.37    && < 0.4-    , network        >= 3.1.2.7   && < 3.2-    , om-show        >= 0.1.2.6   && < 0.2-    , stm            >= 2.5.0.0   && < 2.6-    , text           >= 1.2.5.0   && < 2.2-    , time           >= 1.9.3     && < 1.13-    , tls            >= 1.5.8     && < 1.10+    , aeson                >= 2.0.3.0   && < 2.3+    , base                 >= 4.15.1.0  && < 4.20+    , binary               >= 0.8.8.0   && < 0.9+    , bytestring           >= 0.10.12.1 && < 0.13+    , containers           >= 0.6.4.1   && < 0.7+    , exceptions           >= 0.10.4    && < 0.11+    , megaparsec           >= 9.2.2     && < 9.7+    , monad-logger         >= 0.3.37    && < 0.4+    , network              >= 3.1.2.7   && < 3.2+    , om-fork              >= 0.7.1.9   && < 0.8+    , om-show              >= 0.1.2.6   && < 0.2+    , stm                  >= 2.5.0.0   && < 2.6+    , streaming            >= 0.2.4.0   && < 0.3+    , streaming-binary     >= 0.3.0.1   && < 0.4+    , streaming-bytestring >= 0.3.2     && < 0.4+    , text                 >= 1.2.5.0   && < 2.2+    , tls                  >= 1.5.8     && < 1.10+    , unliftio-core        >= 0.2.1.0   && < 0.3  library   import: warnings, dependencies@@ -52,52 +53,19 @@   hs-source-dirs: src   default-language: Haskell2010 --- This isn't really a test, it is just used to make sure the example--- compiles without clusttering up the package with a bunch of--- executables.-test-suite ingress-example-  import: warnings, dependencies-  main-is: ingress.hs-  type: exitcode-stdio-1.0-  hs-source-dirs: examples-  default-language: Haskell2010-  build-depends:-    , om-socket---- This isn't really a test, it is just used to make sure the example--- compiles without clusttering up the package with a bunch of--- executables.-test-suite egress-example-  import: warnings, dependencies-  main-is: egress.hs-  type: exitcode-stdio-1.0-  hs-source-dirs: examples-  default-language: Haskell2010-  build-depends:-    , om-socket---- This isn't really a test, it is just used to make sure the example--- compiles without clusttering up the package with a bunch of--- executables.-test-suite server-example-  import: warnings, dependencies-  main-is: server.hs-  type: exitcode-stdio-1.0-  hs-source-dirs: examples-  default-language: Haskell2010-  build-depends:-    , om-socket-    , transformers >= 0.5.6.2 && < 0.7---- This isn't really a test, it is just used to make sure the example--- compiles without clusttering up the package with a bunch of--- executables.-test-suite client-example-  import: warnings, dependencies-  main-is: client.hs+test-suite test+  import: dependencies, warnings+  main-is: test.hs   type: exitcode-stdio-1.0-  hs-source-dirs: examples+  other-modules:+    Client+    Egress+    Ingress+    Server+  hs-source-dirs:+    , test+    , examples   default-language: Haskell2010   build-depends:     , om-socket-+    , hspec >= 2.11.1 && < 2.12
src/OM/Socket.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}  {- | Socket utilities. -} module OM.Socket (@@ -24,30 +25,25 @@ ) where  -import Control.Applicative (Alternative((<|>)))-import Control.Concurrent (Chan, MVar, forkIO, newChan, newEmptyMVar,-  putMVar, readChan, takeMVar, throwTo, writeChan)-import Control.Concurrent.STM (TVar, atomically, newTVar, readTVar,+import Control.Concurrent (MVar, forkIO, newEmptyMVar, putMVar, takeMVar)+import Control.Concurrent.STM (TVar, atomically, newTVarIO, readTVar,   retry, writeTVar) import Control.Exception (SomeException, bracketOnError, throw)-import Control.Monad (join, void, when)+import Control.Monad (when) import Control.Monad.Catch (MonadThrow(throwM), MonadCatch, try)-import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad.IO.Unlift (MonadUnliftIO) import Control.Monad.Logger.CallStack (LoggingT(runLoggingT),-  MonadLoggerIO(askLoggerIO), logDebug, logError, logWarn)+  MonadLoggerIO(askLoggerIO), NoLoggingT(runNoLoggingT), MonadLogger,+  logDebug, logError, logWarn) import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey) import Data.Binary (Binary(get), encode) import Data.Binary.Get (Decoder(Done, Fail, Partial), pushChunk,   runGetIncremental) import Data.ByteString (ByteString)-import Data.Conduit ((.|), ConduitT, awaitForever, runConduit, transPipe,-  yield)-import Data.Conduit.Network (sinkSocket, sourceSocket)-import Data.Conduit.Serialization.Binary (conduitDecode, conduitEncode)+import Data.Foldable (traverse_) import Data.Map (Map) import Data.String (IsString) import Data.Text (Text)-import Data.Time (diffUTCTime, getCurrentTime) import Data.Void (Void) import Data.Word (Word32) import GHC.Generics (Generic)@@ -60,58 +56,71 @@ import Network.Socket.ByteString.Lazy (sendAll) import Network.TLS (ClientParams, Context, ServerParams, contextNew,   handshake, recvData, sendData)+import OM.Fork (Race, race) import OM.Show (showt) import Prelude (Applicative(pure), Bool(False, True), Bounded(minBound),   Either(Left, Right), Enum(succ), Eq((/=)), Functor(fmap), Maybe(Just,-  Nothing), Monad((>>), (>>=), return), MonadFail(fail), Semigroup((<>)),-  Show(show), ($), (++), (.), (=<<), IO, Monoid, Num, Ord, String,-  sequence_, snd, userError)+  Nothing), Monad((>>), (>>=), return), MonadFail(fail), Monoid(mempty),+  Semigroup((<>)), Show(show), ($), (++), (.), (=<<), IO, Num, Ord,+  String, flip, snd, userError)+import Streaming (Alternative((<|>)), MFunctor(hoist), MonadIO(liftIO),+  MonadTrans(lift), Of, Stream, join, void)+import Streaming.Binary (decoded)+import Streaming.ByteString (ByteStream, reread) import Text.Megaparsec (MonadParsec(eof), Parsec, many, oneOf, parse,   satisfy) import Text.Megaparsec.Char (char) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL-import qualified Data.Conduit.List as CL import qualified Data.Map as Map import qualified Data.Text as T+import qualified Streaming.Prelude as S import qualified Text.Megaparsec as M  -{- |-  Opens an "ingress" socket, which is a socket that accepts a stream of-  messages without responding.+{-|+  Opens an "ingress" socket, which is a socket that accepts a stream+  of messages without responding. In particular, we listen on a socket,+  accepting new connections, an each connection concurrently reads its+  elements off the socket and pushes them onto the stream. -}-openIngress :: (Binary i, MonadIO m, MonadFail m)+openIngress+  :: forall i m never_returns.+     ( Binary i+     , MonadFail m+     , MonadIO m+     , Race+     )   => AddressDescription-  -> ConduitT () i m ()+  -> Stream (Of i) m never_returns openIngress bindAddr = do     so <- listenSocket =<< resolveAddr bindAddr     mvar <- liftIO newEmptyMVar-    void . liftIO . forkIO $ acceptLoop so mvar-    mvarToSource mvar+    liftIO+      . runNoLoggingT+      . race "ingress accept loop"+      . liftIO+      $ acceptLoop so mvar+    mvarToStream mvar   where-    mvarToSource :: (MonadIO m) => MVar a -> ConduitT () a m ()-    mvarToSource mvar = do-      liftIO (takeMVar mvar) >>= yield-      mvarToSource mvar--    acceptLoop :: (Binary i) => Socket -> MVar i -> IO ()+    acceptLoop :: Socket -> MVar i -> IO ()     acceptLoop so mvar = do       (conn, _) <- accept so       void . forkIO $ feed (runGetIncremental get) conn mvar       acceptLoop so mvar -    feed :: (Binary i) => Decoder i -> Socket -> MVar i -> IO ()-+    feed+      :: Decoder i+      -> Socket+      -> MVar i+      -> IO ()     feed (Done leftover _ i) conn mvar = do       putMVar mvar i       feed (runGetIncremental get `pushChunk` leftover) conn mvar-     feed (Partial k) conn mvar = do       bytes <- recv conn 4096       when (BS.null bytes) (fail "Socket closed by peer.")       feed (k (Just bytes)) conn mvar-     feed (Fail _ _ err) _conn _chan =       fail $ "Socket crashed. Decoding error: " ++ show err @@ -124,13 +133,18 @@   :: ( Binary o      , MonadFail m      , MonadIO m-     , MonadThrow m      )   => AddressDescription-  -> ConduitT o Void m ()-openEgress addr = do+  -> Stream (Of o) m r+  -> m r+openEgress addr stream = do   so <- connectSocket =<< resolveAddr addr-  conduitEncode .| sinkSocket so+  result <-+    S.mapM_+      (liftIO . sendAll so . encode)+      stream+  liftIO (close so)+  pure result   {- | Guess the family of a `SockAddr`. -}@@ -206,118 +220,86 @@   and provides a way to respond to those requests. -} openServer-  :: ( Binary request+  :: forall request response m never_returns.+     ( Binary request      , Binary response+     , MonadLogger m+     , MonadCatch m      , MonadFail m-     , MonadLoggerIO m-     , Show request-     , Show response+     , MonadUnliftIO m+     , Race      )   => AddressDescription   -> Maybe (IO ServerParams)-  -> ConduitT Void (request, response -> m Responded) m ()+  -> Stream (Of (request, response -> m Responded)) m never_returns openServer bindAddr tls = do     so <- listenSocket =<< resolveAddr bindAddr-    requestChan <- liftIO newChan-    logging <- askLoggerIO-    void . liftIO . forkIO . (`runLoggingT` logging) $ acceptLoop so requestChan-    chanToSource requestChan+    requestMVar <- liftIO newEmptyMVar+    lift+      . race "server accept loop"+      $ acceptLoop so requestMVar+    mvarToStream requestMVar   where     acceptLoop-      :: ( Binary i-         , Binary o-         , MonadIO m-         , MonadLoggerIO n-         , Show i-         , Show o-         )-      => Socket-      -> Chan (i, o -> m Responded)-      -> n ()-    acceptLoop so requestChan = do+      :: Socket+      -> MVar (request, response -> m Responded)+      -> m void+    acceptLoop so requestMVar = do       (conn, ra) <- liftIO (accept so)-      (inputSource, outputSink) <- prepareConnection conn       logDebug $ "New connection: " <>  showt ra-      responseChan <- liftIO newChan-      logging <- askLoggerIO-      rtid <--        liftIO-        . forkIO-        . (`runLoggingT` logging)-        $ responderThread responseChan outputSink-      void . liftIO . forkIO . (`runLoggingT` logging) $ do-        result <- try $ runConduit (-            pure ()-            .| transPipe liftIO inputSource-            .| conduitDecode-            .| awaitForever (\req@Request {messageId, payload} -> do-                logDebug $ showt ra <> ": Got request: " <> showt req-                start <- liftIO getCurrentTime-                yield (-                    payload,-                    \res -> do-                      liftIO . writeChan responseChan . Response messageId $ res-                      end <- liftIO getCurrentTime-                      liftIO . (`runLoggingT` logging) . logDebug-                        $ showt ra <> ": Responded to " <> showt messageId <> " in ("-                        <> showt (diffUTCTime end start) <> ")"-                      pure Responded-                  )-              )-            .| CL.mapM_ (liftIO . writeChan requestChan)-          )-        case result of-          Left err -> liftIO $ throwTo rtid (err :: SomeException)-          Right () -> return ()-        logDebug $ "Closed connection: " <>  showt ra-      acceptLoop so requestChan+      (input, send) <- prepareConnection conn+      void . liftIO . forkIO $ handleConnection input send requestMVar+      acceptLoop so requestMVar -    prepareConnection-      :: (MonadIO m)-      => Socket-      -> m (ConduitT Void ByteString IO (), ConduitT ByteString Void IO ())-    prepareConnection conn =-        case tls of-          Nothing -> pure (sourceSocket conn, sinkSocket conn)-          Just getParams ->-            liftIO $ do-              ctx <- contextNew conn =<< getParams-              handshake ctx-              pure (input ctx, output ctx)+    handleConnection+      :: ByteStream IO () {-^ raw bytes input from the socket -}+      -> (BSL.ByteString -> m ()) {-^ How to send bytes back -}+      -> MVar (request, response -> m Responded)+         {-^ how we stream (req, respond) tuples to the client code. -}+      -> IO ()+    handleConnection input send requestMVar =+        void $+          S.mapM_+            sendRequestToMVar+            (decoded input)       where-        output :: Context -> ConduitT ByteString Void IO ()-        output ctx = awaitForever (sendData ctx . BSL.fromStrict)+        sendRequestToMVar :: Request request -> IO ()+        sendRequestToMVar Request { messageId , payload } =+          putMVar+            requestMVar+            ( payload+            , respond messageId+            ) -        input :: Context -> ConduitT Void ByteString IO ()-        input ctx = do-          bytes <- recvData ctx-          if BS.null bytes then-            pure ()-          else do-            yield bytes-            input ctx+        respond :: MessageId -> response -> m Responded+        respond responseTo response = do+          send . encode $ Response { responseTo , response }+          pure Responded -    responderThread :: (-          Binary p,-          MonadLoggerIO m,-          MonadThrow m,-          Show p-        )-      => Chan (Response p)-      -> ConduitT ByteString Void IO ()-      -> m ()-    responderThread chan outputSink = runConduit (-        pure ()-        .| chanToSource chan-        .| awaitForever (\res@Response {responseTo, response} -> do-            logDebug-              $ "Responding to " <> showt responseTo-              <> " with: " <> showt response-            yield res+    {- Maybe make a TLS connection. -}+    prepareConnection+      :: Socket+      -> m+          ( ByteStream IO ()+          , BSL.ByteString -> m ()           )-        .| conduitEncode-        .| transPipe liftIO outputSink-      )+    prepareConnection conn =+      case tls of+        Nothing ->+          pure+            ( rereadNull+                (flip recv 4096)+                conn+            , liftIO . sendAll conn+            )+        Just getParams ->+          liftIO $ do+            ctx <- contextNew conn =<< getParams+            handshake ctx+            pure+              ( rereadNull recvData ctx+              , sendData ctx+              )   {- |@@ -326,7 +308,8 @@   the server. -} connectServer-  :: ( Binary request+  :: forall n request m response.+     ( Binary request      , Binary response      , MonadIO m      , MonadLoggerIO n@@ -339,24 +322,26 @@     logging <- askLoggerIO     liftIO $ do       so <- connectSocket =<< resolveAddr addr-      state <- atomically (newTVar ClientState {-          csAlive = True,-          csResponders = Map.empty,-          csMessageId = minBound,-          csMessageQueue = []-        })-      (send, reqSource) <- prepareConnection so+      state <-+        newTVarIO+          ClientState+            { csServerAlive = True+            , csResponders = Map.empty+            , csMessageId = minBound+            , csRequestQueue = []+            }+      (send, responseSource) <- prepareConnection so       void . forkIO $ (`runLoggingT` logging) (requestThread send state)-      void . forkIO $ (`runLoggingT` logging) (responseThread reqSource state)+      void . forkIO $ (`runLoggingT` logging) (responseThread responseSource state)       return (\i -> liftIO $ do           mvar <- newEmptyMVar           join . atomically $             readTVar state >>= \case-              ClientState {csAlive = False} -> return $+              ClientState {csServerAlive = False} -> return $                 throwM (userError "Server connection died.")-              s@ClientState {csMessageQueue} -> do+              s@ClientState {csRequestQueue} -> do                 writeTVar state s {-                    csMessageQueue = csMessageQueue <> [(i, putMVar mvar)]+                    csRequestQueue = csRequestQueue <> [(i, putMVar mvar)]                   }                 return (takeMVar mvar)         )@@ -366,51 +351,55 @@       for TSL or not depending on the configuration.     -}     prepareConnection-      :: (MonadIO m, MonadIO f)-      => Socket-      -> f (BSL.ByteString -> IO (), ConduitT Void ByteString m ())+      :: Socket+      -> IO+           ( BSL.ByteString -> IO ()+           , ByteStream IO ()+           )     prepareConnection so =         case tls of-          Nothing -> pure (sendAll so, sourceSocket so)+          Nothing ->+            pure+              ( sendAll so+              , rereadNull+                  (flip recv 4096)+                  so+              )           Just params -> do             ctx <- contextNew so params             handshake ctx-            pure (send ctx, reqSource ctx)+            pure (send ctx, resSource ctx)       where         send :: Context -> BSL.ByteString -> IO ()         send = sendData -        reqSource :: (MonadIO m) => Context -> ConduitT Void ByteString m ()-        reqSource ctx = do-          bytes <- recvData ctx-          if BS.null bytes-            then pure ()-            else do-              yield bytes-              reqSource ctx+        resSource+          :: Context+          -> ByteStream IO ()+        resSource = do+          rereadNull recvData -    {- | Receive requests and send them to the server. -}-    requestThread :: (-          Binary i,-          MonadCatch m,-          MonadLoggerIO m-        )-      => (BSL.ByteString -> IO ())-      -> TVar (ClientState i o)-      -> m ()+    {- |+      Receive requests from the client request function and send them+      to the server.+    -}+    requestThread+      :: (BSL.ByteString -> IO ())+      -> TVar (ClientState request response)+      -> LoggingT IO ()     requestThread send state =       join . liftIO . atomically $         readTVar state >>= \case-          ClientState {csAlive = False} -> pure (pure ())-          ClientState {csMessageQueue = []} -> retry+          ClientState {csServerAlive = False} -> pure (pure ())+          ClientState {csRequestQueue = []} -> retry           s@ClientState {-                csMessageQueue = (m, r):remaining,+                csRequestQueue = (m, r):remaining,                 csResponders,                 csMessageId               }             -> do               writeTVar state s {-                  csMessageQueue = remaining,+                  csRequestQueue = remaining,                   csResponders = Map.insert csMessageId r csResponders,                   csMessageId = succ csMessageId                 }@@ -420,47 +409,64 @@      {- |       Receive responses from the server and send then them back to the-      client responder.+      client request function.     -}-    responseThread :: (-          Binary o,-          MonadLoggerIO m,-          MonadCatch m,-          Show o-        )-      => ConduitT Void ByteString m ()-      -> TVar (ClientState i o)-      -> m ()-    responseThread reqSource state = do-      (try . runConduit) (-          pure ()-          .| reqSource-          .| conduitDecode-          .| CL.mapM_ (\r@Response {responseTo, response} ->-              join . liftIO . atomically $-                readTVar state >>= \ ClientState {csResponders} ->-                  case Map.lookup responseTo csResponders of-                    Nothing -> return $-                      logWarn $ "Unexpected server response: " <> showt r-                    Just respond -> return $ liftIO (respond response)+    responseThread+      :: ByteStream IO ()+      -> TVar (ClientState request response)+      -> LoggingT IO ()+    responseThread resSource stateT = do+        try+            (+              void $+                S.mapM_+                  handleResponse+                  (hoist liftIO (decoded resSource))             )-        ) >>= \case-          Left err ->-            logError-              $ "Socket receive error: "-              <> showt (err :: SomeException)-          Right () -> return ()-      join . liftIO . atomically $-        readTVar state >>= \s@ClientState {csResponders, csMessageQueue} -> do-          writeTVar state s {csAlive = False}-          return . liftIO . sequence_ $ [-              r (throw (userError "Remote connection died."))-              | r <--                  fmap snd (Map.toList csResponders)-                  <> fmap snd csMessageQueue-            ]+          >>= \case+            Left err -> do+              logError+                $ "Socket receive error: "+                <> showt (err :: SomeException)+              throw err+            Right () ->+              pure ()+        closeClientState+      where+        closeClientState =+          join . liftIO . atomically $ do+            state <- readTVar stateT+            writeTVar stateT state+              { csServerAlive = False+              , csResponders = mempty+              , csRequestQueue = mempty+              } +            pure . traverse_ liftIO $+              [ respond (throw (userError "Remote connection died."))+              | respond <-+                  Map.elems (csResponders state)+                  <>  fmap snd (csRequestQueue state)+              ] +        handleResponse :: Response response -> LoggingT IO ()+        handleResponse+            responsePackage@Response+              { responseTo+              , response+              }+          = do+              join . lift . atomically $ do+                state <- readTVar stateT+                case deleteFind responseTo (csResponders state) of+                  Nothing ->+                    pure . logWarn $+                      "Unexpected server response: " <> showt responsePackage+                  Just (respond, newResponders) -> do+                    writeTVar stateT state {csResponders = newResponders}+                    pure . lift $ respond response++ {- | A server endpoint configuration. -} data Endpoint = Endpoint {     bindAddr :: AddressDescription,@@ -470,9 +476,9 @@   {- | Response to a request. -}-data Response p = Response {-    responseTo :: MessageId,-      response :: p+data Response p = Response+  { responseTo :: MessageId+  ,   response :: p   }   deriving stock (Generic, Show) instance (Binary p) => Binary (Response p)@@ -509,18 +515,18 @@   {- | Client connection state. -}-data ClientState i o = ClientState {-    csAlive :: Bool,-    csResponders :: Map MessageId (o -> IO ()),-    csMessageId :: MessageId,-    csMessageQueue :: [(i, o -> IO ())]+data ClientState i o = ClientState+  {  csServerAlive :: Bool+  ,   csResponders :: Map MessageId (o -> IO ())+  ,    csMessageId :: MessageId+  , csRequestQueue :: [(i, o -> IO ())]   }   {- | A Request message type. -}-data Request p = Request {-    messageId :: MessageId,-      payload :: p+data Request p = Request+  { messageId :: MessageId+  ,   payload :: p   }   deriving stock (Generic, Show) instance (Binary p) => Binary (Request p)@@ -533,13 +539,6 @@   deriving newtype (Binary, Num, Bounded, Eq, Ord, Show, Enum)  -{- | Construct a coundiut source by reading forever from a 'Chan'. -}-chanToSource :: (MonadIO m) => Chan a -> ConduitT Void a m ()-chanToSource chan = do-  yield =<< liftIO (readChan chan)-  chanToSource chan-- {- |   Proof that a response function was called on the server. Mainly   useful for including in a type signature somewhere in your server@@ -547,5 +546,43 @@   request in all cases. -} data Responded = Responded+++mvarToStream+  :: (MonadIO m)+  => MVar i+  -> Stream (Of i) m never_returns+mvarToStream mvar = do+  liftIO (takeMVar mvar) >>= S.yield+  mvarToStream mvar+++rereadNull+  :: (Monad m)+  => (c -> m ByteString)+  -> c+  -> ByteStream m ()+rereadNull f =+  reread+    (\c -> do+      bytes <- f c+      pure $ if BS.null bytes then Nothing else Just bytes+    )+++{-|+  If the key exists in the map, delete it and return its value along+  with the new map.+-}+deleteFind+  :: (Ord k)+  => k+  -> Map k v+  -> Maybe (v, Map k v)+deleteFind key m =+  case Map.lookup key m of+    Nothing -> Nothing+    Just v ->+      Just (v, Map.delete key m)  
+ test/test.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar,+  threadDelay)+import Control.Monad (void)+import Control.Monad.Catch (MonadCatch)+import Control.Monad.IO.Unlift (MonadIO(liftIO), MonadUnliftIO)+import Control.Monad.Logger (MonadLogger, runStdoutLoggingT)+import Data.Function ((&))+import Data.Text (Text)+import OM.Fork (Race, runRace)+import OM.Show (showt)+import OM.Socket (Responded, connectServer, openEgress, openIngress,+  openServer)+import Prelude (Applicative(pure), Foldable(length), Functor(fmap),+  Maybe(Nothing), Traversable(sequence), ($), (.), (<$>), (=<<), IO,+  Int, MonadFail, fst, print, reverse, seq, sequence_)+import Streaming.Prelude (Of, Stream)+import Test.Hspec (hspec, it, shouldBe)+import qualified Client+import qualified Egress+import qualified Ingress+import qualified Server+import qualified Streaming.Prelude as Stream+++main :: IO ()+main =+  hspec $ do+    it "ingress/egress" $ do+      let+        expected :: [Int]+        expected = [1, 2, 3, 4]+      block <- newEmptyMVar+      void . forkIO $ do {- Ingress thread -}+        let+          instream :: (Race) => Stream (Of Int) IO ()+          instream = openIngress "localhost:9933"+        {-+          The instream never closes, because an ingress server is meant+          to listen forever, so we consume only the expected number+          of items from the stream+        -}+        actual <-+          runRace $+            Stream.take (length expected) instream & Stream.toList_+        putMVar block actual++      {- Push the expected items over the network.  -}+      Stream.each expected & openEgress "localhost:9933"+      actual <- takeMVar block+      print actual+      actual `shouldBe` expected++    it "client/server" $ do+      let+        requestStream+          :: ( MonadCatch m+             , MonadFail m+             , MonadLogger m+             , MonadUnliftIO m+             , Race+             )+          => Stream (Of (Int, Text -> m Responded)) m void+        requestStream = openServer "localhost:9934" Nothing++        requests :: [Int]+        requests = [1..4]++        expected :: [Text]+        expected = showt <$> requests++      block <- newEmptyMVar+      void . forkIO $ runRace $ runStdoutLoggingT $ do+        {-+          Handle the requests in reverse order to test out of order+          handling.+        -}+        reqs <-+          fmap reverse+          . Stream.toList_+          . Stream.take (length expected)+          $ requestStream+        liftIO (print (fst <$> reqs))+        sequence_+          [ respond (showt request)+          | (request, respond) <- reqs+          ]+        liftIO (putMVar block ())+      call <- runStdoutLoggingT (connectServer "localhost:9934" Nothing)+      calls <-+        sequence+          [ do+              threadDelay 100_000+              response <- newEmptyMVar+              void . forkIO $+                putMVar response =<< call i+              pure response+          | i <- requests+          ]+      responses <-+        sequence+          [ takeMVar response+          | response <- calls+          ]+      print responses+      responses `shouldBe` expected+      takeMVar block++    it "make sure the examples compile" $+      {-+        We don't actually _run_ the examples. Really it is the imports+        that makes sure the examples compile. We `seq` them here just+        so we don't get warnings about unused imports.+      -}+      Client.main+      `seq` Server.main+      `seq` Ingress.main+      `seq` Egress.main+      `seq` (pure () :: IO ())+