core icon indicating copy to clipboard operation
core copied to clipboard

Ports called asynchronously if inside Cmd.batch

Open lazamar opened this issue 7 years ago • 1 comments

tl;dr

  • Elm 0.19 allows catching events and sending them through ports in the same tick of the event loop.
  • If we use Cmd.batch the port call does not happen in the same tick of the event loop.

Problem Statement

Certain DOM events need to be handled synchronously. Elm now allows us to do that through a port. However, if when dispatching the event to a port we use Cmd.batch and send other events with it, this port call is now handled asynchronously making the handling of certain events not possible again.

Here are the return values I tested for the update function and their outcome.

  • (model, sendToPort event) - gets called synchronously as expected
  • (model, Cmd.batch [ sendToPort event] ) - gets called synchronously as expected
  • (model, Cmd.batch [ sendToPort event, otherCmd ]) - gets called asynchronously, unlike expected.

If we use Cmd.batch with a list containing only the port command, the call is still synchronous. This means that the amount of elements in the batch list changes the program's behaviour.

SSCE

The full code is here https://ellie-app.com/3nJ8Pz5p7kba1, however, for some reason Ellie is not handling the synchronous port call correctly. I recorded the gif running the exact same code in Elm 0.19.0 locally.

In this example I'm using event.dataTransfer.setData, which must be called synchronously in the callback for the dragstart event.

  • If called synchronously it adds the data specified to event.dataTransfer.items.
  • If called asynchronously the invocation of setData is silently ignored.

peek 2018-09-18 18-13 - bigger

Important elm bit

port handleEvent : Decode.Value -> Cmd msg

type Msg = Noop  | WithoutBatch Decode.Value | WithBatch Decode.Value

update msg model =
    case msg of
        WithoutBatch event -> ( model, handleEvent event )
        WithBatch event    -> ( model, Cmd.batch [ handleEvent event, anotherCmd])
        Noop               -> ( Model, Cmd.none )

anotherCmd = Task.perform identity (Task.succeed Noop)

The JS part.

var app = Elm.Main.init({ node: document.querySelector('main') })

app.ports.handleEvent.subscribe(function addItemToEvent(event) {
    console.log("Before adding item:", event.dataTransfer.items.length);
    event.dataTransfer.setData("elm", "rocks");
    console.log("After adding item:", event.dataTransfer.items.length);
});

lazamar avatar Sep 18 '18 17:09 lazamar

I tracked it down to a more specific issue. If I dispatch Task.perform identity (Task.succeed MyMsg) together with a port, the port call becomes asynchronous.

Here is a demo that works in Ellie showing that removing the extra command makes port call synchronous again. https://ellie-app.com/3nJ3z59nCdQa1

peek 2018-09-18 19-10

lazamar avatar Sep 18 '18 18:09 lazamar