Skip to content

[BUG] Non-deterministic port initialization order causes missed messages #132

@decioferreira

Description

@decioferreira

Describe the bug
When returning cmds and cause new subscriptions, it is not quite defined in which order the cmds and subscriptions are processed.

To Reproduce
Example can be found here: https://ellie-app.com/wX8PpqvLsH8a1

Steps to reproduce the issue (please include code snippets, input files, or commands if possible):

  1. Open the console and press +1. You should see msg: GotIt "from JS".
  2. Then flip the commented out code in init. Suddenly you won’t see that log when pressing +1 anymore.

That’s because the init change causes the ports to be registered in the opposite order in the compiled JS, and that determines whether the port subscription is processed before the port command.

Expected behavior
Port messages should be delivered consistently, regardless of unrelated changes to the program structure or where port commands are called.

Actual behavior / error output
After adding or moving port calls (for example, inside init), Elm sometimes fails to receive messages from JavaScript, even though the ports are correctly defined and connected.

Example Code or Project

Elm (Main.elm):

port module Main exposing (main)

import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)


port getIt : String -> Cmd msg


port gotIt : (String -> msg) -> Sub msg


type alias Model =
    { count : Int }


init : () -> ( Model, Cmd Msg )
init () =
    ( { count = 0 }
      -- Flip to the `getIt` line here to trigger the bug.
    --, Cmd.none
    , getIt "via init"
      -- In this case, calling `getIt` here is useless.
      -- But imagine adding another `getIt` call in a bigger app
      -- and suddenly the original use breaks. Oops!
    )


type Msg
    = Increment
    | Decrement
    | GotIt String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case Debug.log "msg" msg of
        Increment ->
            ( { model | count = model.count + 1 }, getIt "via update Increment" )

        Decrement ->
            ( { model | count = model.count - 1 }, Cmd.none )

        GotIt _ ->
            ( model, Cmd.none )


subscriptions : Model -> Sub Msg
subscriptions model =
    if model.count > 0 then
        gotIt GotIt

    else
        Sub.none


view : Model -> Html Msg
view model =
    div []
        [ p [] [ text """Open the console and press +1. You should see `msg: GotIt "from JS"`.""" ]
        , p [] [ text """Then flip the commented out code in `init`. Suddenly you won’t see that log when pressing +1 anymore.""" ]
        , p [] [ text """That’s because the `init` change causes the ports to be registered in the opposite order in the compiled JS, and that determines whether the port subscription is processed before the port command.""" ]
        , button [ onClick Increment ] [ text "+1" ]
        , div [] [ text <| String.fromInt model.count ]
        , button [ onClick Decrement ] [ text "-1" ]
        ]


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

HTML (index.html):

<html>
<head>
</head>
<body>
  <main></main>
  <script>
    var app = Elm.Main.init({ node: document.querySelector('main') })
    app.ports.getIt.subscribe(s => {
      console.log("app.ports.getIt", s);
      app.ports.gotIt.send("from JS");
    });
  </script>
</body>
</html>

Additional context

Reported here:

A proposed fixed was done here: https://discord.com/channels/534524278847045633/731183487775932478/1432860816423518308

@@ -1938,11 +1938,15 @@ function _Platform_setupEffects(managers, sendToApp)
 {
        var ports;
 
+       // sort the entries in _Platform_effectManagers to ensure consistent order
+       var valFun = (m) => m.a ? m.e ? 2 : 3 : 1;
+       var sorted = Object.entries(_Platform_effectManagers).sort(
+               ([, m1], [, m2]) => valFun(m1) - valFun(m2)
+       );
+
        // setup all necessary effect managers
-       for (var key in _Platform_effectManagers)
+       for (var [key, manager] of sorted)
        {
-               var manager = _Platform_effectManagers[key];
-
                if (manager.a)
                {
                        ports = ports || {};

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions