Callbacks to Aff

I’m currently working on a sever-side project that communicates with a server using TCP Sockets.
After a quick search on pursuit, I landed on using purescript-node-net package. I looked at the /tests/ directory to look for examples no how to use sockets in Purescript
and found that node sockets relies on callbacks, such as onError, onData and onClose.
I’m having trouble understanding how to consume callbacks and extract data from them to parent callers.
Consider this example:

import Prelude

import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (errorShow, infoShow, logShow)
import Node.Buffer (toString)
import Node.Encoding (Encoding(..))
import Node.Net.Server as Node.Net.Server
import Node.Net.Socket as Node.Net.Socket

main = do

    log "Program started"

    socket <- Node.Net.Socket.createConnectionTCP 8080 "localhost" do
              infoShow { _message: "Connected to server" }

    Node.Net.Socket.onReady socket do
        infoShow { _message: "Socket is ready" }
        void $ Node.Net.Socket.writeString socket "some JSON data goes here" UTF8 mempty
    
    Node.Net.Socket.onClose socket case _ of
      false -> infoShow { _message: "Socket closed without an error" }
      true -> errorShow { _message: "Socket closed with an error" }

    Node.Net.Socket.onData socket case _ of
      Left buffer -> do
        bufferString <- toString UTF8 buffer
        logShow { _message: "Received some data", bufferString }
      Right string -> logShow { _message: "Received some data", string }
    Node.Net.Socket.onError socket \err ->
      errorShow { _message: "Socket had an error", err }

    -- how to access bufferString and err in this scope level???
      
    log "Program finished"

When I run this program, I see “Program started” and immediately “Program finished” and then I see other callbacks execute.
I understand this is how they’re supposed to be. But is there a way to convert these callback to run under Aff so that bufferString and err are exposed?
Something like this in psuedo code:

main = do
    log "Program started"
    
    socket <- Node.Net.Socket.createConnectionTCP 8080 "localhost"
    resultOrError <- Node.Net.Socket.writeString socket "some JSON data goes here" UTF8 mempty 
    
    log "Program finished"

My main goal is to get the values AND guarantee that all callbacks run between “Program started” and “Program finished”.
I hope make explanation makes sense.

The simplest approach to get the desired effect doesn’t even need Aff. You can just wrap the onData case with Right, and the onError case with Left and call the same callback function from both. Then the program could run a “finish” block at the end of that callback:

main = do

    log "Program started"

    socket <- Node.Net.Socket.createConnectionTCP 8080 "localhost" do
              infoShow { _message: "Connected to server" }

    Node.Net.Socket.onReady socket do
        infoShow { _message: "Socket is ready" }
        void $ Node.Net.Socket.writeString socket "some JSON data goes here" UTF8 mempty
    
    Node.Net.Socket.onClose socket case _ of
      false -> infoShow { _message: "Socket closed without an error" }
      true -> errorShow { _message: "Socket closed with an error" }

    Node.Net.Socket.onData socket \input -> processTraffic $ Right input
    Node.Net.Socket.onError socket \err -> processTraffic $ Left err

processTraffic :: Either String (Either Buffer String) -> Effect Unit
processTraffic traffic = do
  case traffic of
    Right receivedData -> 
      case receivedData of
        Left buffer -> do
          bufferString <- toString UTF8 buffer
          logShow { _message: "Received some data", bufferString }
        Right string -> logShow { _message: "Received some data", string }
    Left err -> 
      errorShow { _message: "Socket had an error", err }

  log "Program finished"

Now if you wanted to turn that into an Aff, you could probably use the makeAff function:

getData :: Socket -> Aff (Either Buffer String)
getData socket = makeAff $ \callback -> do
    Node.Net.Socket.onData socket (\receivedData -> callback $ Right receivedData)
    Node.Net.Socket.onError socket (\err -> callback $ Left $ error err)

    pure mempty

that said, I haven’t tried that code so I might be missing something. The makeAff definition:
makeAff :: forall a. ((Either Error a -> Effect Unit) -> Effect Canceler) -> Aff a
can be a bit tricky to wrap your head around. So you have to pass it a function that takes another function as its input. The function that gets passed into your function is the callback that you must invoke with Either Error a. In our case, a is what we get from onData, so a = Either Buffer String. To turn that into Either Error a, we’ve got to wrap it in Right. The error case is slightly trickier since onError is only giving us a String, so in addition to wrapping it in Left, we also have to turn that String into a JavaScript Error first. Finally you return a Canceler. In the docs for the canceler, it says "A no-op Canceler can be constructed with mempty", which is what I did there.
(Note, some are of the school of thought that you should try to make your Aff never throw an Error, and return an Either that covers the error case. This is doable with slight modifications that I will leave as an exercise to the reader).
With this function, you could make your writeString function:

writeString socket message encoding writeCallback = do
  liftEffect $ Node.Net.Socket.onReady socket do
    infoShow { _message: "socket is ready" }
    void $ Node.Net.Socket.writeString socket message encoding writeCallback

  getData socket

and then your main:

main = launchAff_ do

    log "Program started"

    socket <- liftEffect $ Node.Net.Socket.createConnectionTCP 8080 "localhost" (infoShow { _message: "Connected to server" })
    
    liftEffect $ Node.Net.Socket.onClose socket case _ of
      false -> infoShow { _message: "Socket closed without an error" }
      true -> errorShow { _message: "Socket closed with an error" }

    resultOrError <- try $ writeString socket "some JSON data goes here" UTF8 mempty
    liftEffect $ case resultOrError of
      Right receivedData -> 
        case receivedData of
          Left buffer -> do
            bufferString <- toString UTF8 buffer
            logShow { _message: "Received some data", bufferString }
          Right string -> logShow { _message: "Received some data", string }
      Left err -> 
        errorShow { _message: "Socket had an error", err }

  log "Program finished"

I’m not sure that gets you a whole lot of added benefit though above just using callbacks directly…

2 Likes

I went with the second solution and it worked like a charm.
Thank yo so much.

2 Likes