Search code examples
node.jsportselm

How to create worker with elm 0.17


For elm 0.16, I just defined some ports (that are just Signal of data) without main function and used Signal.map to process data back and forth. It is just for data processing called from nodejs (not from browser), so I do not depend on Html module.

Now elm 0.17 has Cmd and Sub instead of Signal, I cannot figure out how to do the same...

Can anybody give me an simplest example to do data processing via ports exposed by worker initialization with elm 0.17?


Here is my simplest example with Elm 0.16...

elm 0.16 code:

module Main where
import Signal
import String exposing (isEmpty, reverse)

-- input ports
port jsToElm : Signal String

-- output ports
port elmToJs : Signal String
port elmToJs
  = Signal.map String.reverse jsToElm

javascript (es6) code:

/** main */
const Elm = loadElm('./index.js') // custom function to eval Elm code
const app = Elm.worker( Elm.Main, { jsToElm: ''})

app.ports.jsToElm.send('test')

app.ports.elmToJs.subscribe(( txt ) => {    
    console.log( txt )
})

Solution

  • Update

    There is now a package that gives you the ability to create a worker. See lukewestby/worker

    Original Answer

    I was able to get a working example by creating an Html.App program. I can't find any way around the requirement of having a main function that results in a program that includes a view function, and I'm not alone in that confusion.

    My example here is browser based, hopefully this fits back into your node-based version; I just haven't used the node version before.

    Main.elm:

    port module Main exposing (..)
    
    import Json.Decode
    import Json.Encode
    import Html exposing (..)
    import Html.App
    
    import String exposing (isEmpty, reverse)
    
    -- input ports
    port jsToElm : (String -> msg) -> Sub msg
    
    -- output ports
    port elmToJs : String -> Cmd msg
    
    type alias Flags = { jsToElm : String }
    
    main =
      Html.App.programWithFlags
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }
    
    type alias Model = { text : String }
    
    init : Flags -> (Model, Cmd Msg)
    init flags =
      (Model flags.jsToElm, Cmd.none)
    
    type Msg
      = Reverse String
    
    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
      case msg of
        Reverse str ->
          let reversed = reverse str
          in ({ model | text = reversed }, elmToJs reversed)
    
    view : Model -> Html.Html Msg
    view model =
      text <| "text is: " ++ model.text
    
    subscriptions : Model -> Sub Msg
    subscriptions _ =
      jsToElm Reverse
    

    index.html:

    <script type="text/javascript" src="Main.js"></script>
    <script type="text/javascript">
    var app = Elm.Main.fullscreen({
      jsToElm: 'first'
    });
    
    window.setTimeout(function() {
      app.ports.jsToElm.send('test');
    }, 1);
    
    app.ports.elmToJs.subscribe(function ( txt ) {    
        console.log( txt )
    });
    </script>
    

    A couple takeaways:

    1. As you can see, the simple mapping of Signals is gone and there is much more boilerplate for such a small example. The idea is that you set up your subscription to the port, then send a Cmd inside the update function when you want to send information back to Javascript.
    2. This whole Cmd/Sub thing requires a Program and the only way to do that (that I could find) was to include [Html.App]. I have a hunch that in the future, the core Router type can be used to make a "headless" worker, but it seems like for now we're stuck with requiring the Html.App program and having to render a view.
    3. You'll notice the setTimeout call when sending "test" to the port in javascript. I'm not sure why this kludge is necessary but others have seen it before as well