Trouble with text inputs and double update roundtrips

I’ve run into strange behavior of text input elements in combination with “double update” - i.e. process Msg from input, issue a Cmd Msg and update model in the subsequent update, like this:

| user input | -- Msg1 value --> | update (model unchanged yet) | -- Cmd (Msg2 value) --> | update (model changed) |

Following is some background.

In my app I have a complex type representing a document. There are many possible transformations that can be applied to it. For versioning purposes every time any transformation is applied I need to capture and store the current time. I want my type to be opaque and transformations to be atomic. But to get time I need to perform a Time.now task and get another Msg with time. I can’t think about any other way than having two updates for every input event.

I came up with something like this:

apply : (Doc -> Doc) -> Doc -> Task Never Doc
apply transform doc =
    Task.map (\time -> transform { doc | version = time }) Time.now

Now when user types in an input I issue an InputChanged message and in the update I return a Cmd constructed with the above apply function. This works, except when the user types in the middle of the text the cursor jumps to the end of the input. Also undo is not working anymore.

Here is a reduced example: https://ellie-app.com/bbLDGML6PbFa1

In writing this I have two goals:

  1. I suppose I should open an issue somewhere, but I’m not sure where.

    In the elm/virtual-dom, elm/html or somewhere else? Or am I just doing it wrong?

  2. I’d like to find a nice way to structure this flow. Any suggestions?

I’ve made an example where the cursor does not jump.

https://ellie-app.com/bbMMCjXngNba1

In general I think one should avoid passing the Model in a Msg since this can cause tricky concurrency behaviours that are hard to reason about.

1 Like

If you are using the timestamp to version documents, in order to ensure that each version is uniquely tagged and ordered, might you be able to achieve the same thing by using a simple counter? That is, instead of getting the timestamp on each document update, just store a current version as an Int in the model and increment it each time?

I would second this - I made an Ellie to show what can happen if multiple events fire in quick succession. A later update can overwrite the model with stale data from an earlier state:

https://ellie-app.com/yVC9wPk33xa1

Thank you for your input. I really appreciate it!

You (and @rupert) are right about this. Please consider that my Ellie is a very reduced example. As shown in the apply function I only pass the Doc, not the whole Model!

The problem with your solution @albertdahlin is that the update is not atomic. Basically you change the text and then the time separately. In the real app I have tens of possible events and transformations. Each has to be handled in the update function. Also the document is in a Maybe, so every time I have to account for it being Nothing. In my current solution (that breaks input elements) I just return:

let
    transform =
        Doc.setText value
in
( model
, model.doc
    |> Maybe.map (Doc.apply transform)
    |> Maybe.map (Task.perform DocTransformationApplied)
    |> Maybe.withDefault Cmd.none
)

I want to make it impossible to update the document without updating the version (as the guru said “make invalid states impossible”).

If I won’t find a better way I will do it like you suggested, but I hope to get keep the updates atomic.

It would solve the issue of double updates, but it has two downsides. (1) If two users update the document concurrently there might be a version clash. This is very unlikely with a timestamp. I could use something like UUID, but then I’m loosing ability to order by version (2) Having version as date is user friendly. It’s nicer to show

this happened when version from Friday, 13:22 was published

than

this happened when version 23553 was published

consider that every keystroke in any input increments the version, so numbers will get huge very quickly.

That is a good example of tricky concurrency that is not obvious at first.

I would like to elaborate on some things

  • The view and the DOM is always “behind” the Model. It will update at most every animation frame (commonly every 16ms but might be slower) and always run after the call to update
  • Multiple messages might be processed (batched) before the view
  • The order in which events from the DOM are received/processed is difficult to reason about and might vary across browsers (I don’t have any evidence for this, it’s just an assumption).
  • The unpredictability of message ordering also applies to some commands (Cmd), for example Http requests. Here is an example, do you see the bug or the problem?
type Msg
    = SearchInputChanged String
    | GotSearchSuggestions (Result Http.Error (List String))

update msg model =
    case msg of
        SearchInputChanged newText ->
            ( { model | searchText = newText }
            , fetchSuggestionsFromServer newText
            )

        GotSearchSuggestions (Ok suggestions) ->
            ( { model | suggestions = suggestions }
            , Cmd.none
            )

        GotSearchSuggestions (Error_) ->
            ( model
            , Cmd.none
            )

fetchSuggestionsFromServer text =
    Http.get
        { url = "http://example.com/?search=" ++ text
        , expect = Http.expectJson GotSearchSuggestions (Json.Decode.list Json.Decode.string)
        }

The problem is the assumption that the response from the server will come in the order requests where sent. If two requests are sent and the first takes longer for the server to process the responses will arrive in reversed order. This will result in model.suggestions containing stale data.

A solution here is to include the search text in the reply msg by adding an argument to GotSearchSuggestions:

type Msg
    = SearchInputChanged String
    | GotSearchSuggestions String (Result Http.Error (List String))

Now you can check if the suggestions you received match model.searchText and if not, throw the result away.

1 Like

The update function in Elm is single threaded - as javascript in the browser is single threaded. This means that it cannot have a race condition where 2 updates get the same value - the increments will be ‘atomic’.

I would actually say that using a timestamp could result in 2 updates getting the same timestamp. If they were processed in succession very quickly, they might end up getting the same millisecond timestamp. I am not sure though, how quickly in succession Elm can call Time.now, and whether this it is really possible to get the same timestamp out of it.

Right. That’s the good part of your solution - it’s atomic. But I was thinking about a scenario where two users open the document (version 1). Each of them makes one change (maybe one removes a section and the other inserts a comma). Each of them have version 2 now, but they are two different versions. With timestamps it’s very unlikely.

Also the semantic nature of timestamp is really good in my application. Granted that in some applications a counter, checksum, etc would be preferable. But I’ve put some thought into this and believe the timestamp will work the best in the overall design of the system.

For now I do roughly what @albertdahlin suggested, but I’m still hoping for a better solution.

If you really want to go with time stamps, you might also try going with a hybrid approach where you first assign a numerical version number, so you can update the model immediately and at the same time request a time stamp for it. Once the time stamp arrives, you replace the version number by the time stamp. So you would have

type Version = Preliminary Int | Permanent TimeStamp

and a message GetTimeStamp Int TimeStamp (so you can combine the right time stamp with the right preliminary version number).

1 Like

As I see it, there are two problems here:

  • Atomic updates
  • The cursor jumps

My (partial) understanding of your problem with atomic updates is that you need both a string and a time. In other words, you need to unambiguously pair a text with the corresponding timestamp.

The cursor jumping problem occurs when the virtual dom updates the content of the text area. As far as I understand, this only happens if the value in the model is different from the <textarea value=""> in the DOM. The vdom only updates the textarea if the values are different. When using tasks with Time.now the resulting update seems to happen in the next animation frame.

My guess is that the flow of events would look something like this:

  1. User changes the input and an event fires.
  2. update in elm app is called and returns a Cmd to get the current time
  3. view is executed but the text in the DOM differs from the text in the elm model since the model was not updated.
  4. update in elm is called with the new time and the text is updated in the model.
  5. view is called and changes the textarea again

Maybe a solution could be to separate your Doc type and the view state for the textarea? Something along these lines:

type alias Model =
    { text : String -- populate this with the string represantation from Doc when it changes?
    , doc : Doc
    }

type Msg
    = TextareaChanged String
    | GotTimeFor String Time.Posix

update msg model =
    case msg of
        TextareaChanged text ->
            ( { model | text = text }
            , Task.perform (GotTimeFor text) Time.now
            )

        GotTimeFor text time ->
            handle your atomic update
1 Like

Yes! I think your technical explanation is also correct. Basically if I type b here: a|c (pipe represents the cursor), then the browser immediately updates the DOM value to abc, but Elm update brings it back to ab (in accordance with the model that didn’t change yet). Then the time message comes in (probably in the next animation frame as you suggested) and the text input gets the final final value of abc. Kind of back and forth. From the browser’s perspective the last two updates are not triggered by input event and this must be throwing off the cursor.

If this is correct then only two ways to prevent it is to either make Time.now synchronous (probably impossible) or update the model in the first pass of the update and later update the version.

I really like the solution suggested by @eike (and in part @rupert). In fact I can delay the whole timestamp business until the user exports the document (in my case it’s either uploading it to the server or downloading a file).

So all I need to do is count the changes since the document was loaded. And since I don’t even care about how many times it was changed, only that it was, I can represent the version as Maybe Time.Posix where Nothing indicates that the document was modified since last export. And when the document is exported check if it was modified (version == Nothing). If so, then update the version before it’s saved. If it’s Just version then keep it as it was.

This should also improve performance when typing - only one update per keystroke!

Thank you all your input. It will help me to model my program better!

It occurs to me that you might find this Elm Europe talk interesting:

1 Like

Very interesting indeed! Thanks for sharing.

How about this?
https://ellie-app.com/bdXzGmL5S6Va1

  • only uses one call to update
  • gets the exact time of the user’s input
  • no problems with the cursor

Instead of using onInput, you useon "input" and provide you own decoder for the event. Events have a timeStamp property that tells you when the event occurred relative to the time origin (in ms). You can pass the time origin into elm via a flag (using performance.timeOrigin) and then use that to get the absolute timestamp for your event in the update function.

1 Like

It is actually enough to just use something like Html.Events.on "input" Html.Events.targetValue instead of Html.Events.onInput.

As explained in the notes here, onInput creates a synchronous event handler and makes sure all commands are run immediately after update, to make sure the browser treats them as “happening after user interaction”. This bites you here:

  1. User types something, TextUpdated is fired synchronously.
  2. update recieves your message, returns the old contents of the <textarea>.
  3. view is also run synchronously, to prevent the model and the view from diverging
  4. Your command is executed, goes through update, and causes the view to change again to the desired state. Since the VDOM caused the textarea to update twice, history and selection is lost.

So it may be a bug in the runtime, since it calls view before running synchronous commands. I’m not sure what the intended behaviour in that case is.

Here is an ellie logging what is happening here: https://ellie-app.com/bf3fGX2pLCJa1

grafik

it also shows a way of getting the timestamp synchronously by (ab)using Json.Decode, maybe don’t do this.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.