How to go from `(Event -> Effect a) -> Effect EventListener` to `(Event -> Aff a) -> Aff EventListener`?

In trying to FFI with async js functions, I use the aff-promise package to turn them into Aff. But the Web.Event.eventListener has the type:

eventListener :: forall a. (Event -> Effect a) -> Effect EventListener

How to feed an Event -> Aff a to it and get back an Aff EventListener?

Also, type guarantees aside, is it semantically safe to do so? We are talking about mixing sync with async here.

1 Like

you probably want to use launchAff_ in the callback to go from Aff a to Effect Unit. The resulting effect will always be sync (it’s just wrapping up the listener or whatever, for the sake of referential transparency), but you can lift it into Aff using liftEffect

2 Likes

Basically I did half of what you suggested right now: only launchAff_ the relevant part and the whole program stays in the Effect. Then I got ambitious and wanted to go fancier with Aff.bracket and such, the whole program goes Aff and is launched at the end.

If possible, I’d like to avoid launchAff_, liftEffect then launchAff_ again.

The type of eventListener is not very conducive to a clean lifting into Aff, because once the EventListener is returned the Aff is complete. New Affs produced by the callback would either have to be launched as entirely distinct Aff processes—using launchAff_ as described does this—or have to be incorporated somehow into an outer Aff, but that can’t be the Aff that returns the EventListener, for obvious reasons.

You’ll probably have an easier time Affifying around an abstraction that wraps eventListener, addEventListener, and removeEventListener (in case of cancellation or errors) all at once; then you can return an Aff a that blocks forever until canceled or errored.

2 Likes

Alternatively, you could create a wrapper which simply pushes events to an AVar and lets you consume said events in the outer aff.

1 Like

TIL AVar exists. If I read the documentation correctly, there is a problem. Aff.AVar.take doesn’t take a callback in Aff, only AVar.take accepts a callback AVarCallback, which is till in Effect:

type AVarCallback a = Either Error a -> Effect Unit

Doesn’t it defeat the whole purpose in this particular case?

I was indeed mistaken! It worked and the skeleton of the code was roughly:

module Main where

import Prelude

import Control.Monad.Rec.Class (forever)
import Data.Foldable (for_)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Aff as Aff
import Effect.Aff.AVar as Aff.AVar
import Effect.AVar as AVar
import Effect.Class (liftEffect)
import Web.Event.Event as WEE
import Web.Event.EventTarget (addEventListener, eventListener)
import Web.Socket.WebSocket as WS
import Web.Socket.Event.EventTypes as WSEE

main :: Effect Unit
main = launchAff_ main'

main' :: Aff Unit
main' = do
  queue <- Aff.AVar.empty
  setupListeners queue
  forever do
    { ty, event } <- Aff.AVar.take queue
    case ty of
      _ -> pure unit

type Event = { ty :: WEE.EventType, event :: WEE.Event }

setupListeners :: AVar.AVar Event -> Aff Unit
setupListeners queue = Aff.bracket connect disconnect listen
  where
  connect =
    WS.create
      "wss://..."
      [ WS.Protocol "wss" ]
      # liftEffect

  disconnect _conn = pure unit

  listen conn = liftEffect do
    let
      target = WS.toEventTarget conn
      on ty event = AVar.put { ty, event } queue (const $ pure unit)

    for_
      [ WSEE.onOpen
      , WSEE.onMessage
      , WSEE.onError
      , WSEE.onClose
      ]
      \ty ->
        eventListener (on ty) >>= \l -> addEventListener ty l true target

A websocket client program as you can see. Now the main body of the logic lives in Aff (the forever part) and there are only a handful liftEffects to lift websocket related code to Aff.

1 Like

I can’t take a detailed look at the code right now, but I did use avars to communicate using websockets!

AVar.take does not take a callback, and that’s the point! It blocks until a value exists, which is often what you want. You can do things like fork-ing your Aff in order to do other stuff in the meantime. You can also use parOneOf to do things like putting a timeout on your AVar.take.

EDIT: I read your message wrong! Glad you got it working in the end:)

1 Like