egdvnyjklu
egdvnyjklu

Reputation: 313

Avoid Http Race Condition in Elm

Let's assume we have a text input field and on every change of its content we send an Http request to a search API. Now, we don't have any guarantee that the Http responses get back to elm in the same order that we sent the requests.

What's the easiest way to make sure we react to the response corresponding to the latest request – rather than the latest response, which might correspond to an outdated search string? Is there an easy way to attach the query string to the message returned by Elm's http effect? Or any other way we can link the response to the request by which it was triggered?

I'd like to avoid including the query in the response of the search API if possible. Another remedy would be to debounce the search, but that would just decrease the probability of using the wrong response, whereas we'd like to eliminate it.

Thanks for your help!

Example:

import Html
import Html exposing (..)
import Html.Events exposing (onClick, onInput)
import Http
import Json.Decode as Decode


main = Html.program
        { init = ( { searchText = "", result = "" }, Cmd.none )
        , update = update
        , subscriptions = (\model -> Sub.none)
        , view = view
        }


type alias Model =
    { searchText : String
    , result: SearchResult  
    }


type alias SearchResult = String


type Msg 
    = NewSearchText String
    | ReceivedResponse (Result Http.Error SearchResult)


update msg model = 
    case msg of 

        NewSearchText newText ->
            ( { model | searchText = newText}
            , getSearchResult newText
            )

        ReceivedResponse (Result.Ok response) ->
            ( { model | result = response }
            , Cmd.none
            ) 

        ReceivedResponse (Result.Err error) ->
            Debug.crash <| (toString error)



getSearchResult : String -> Cmd Msg
getSearchResult query =
    let 
        url = "http://thebackend.com/search?query=" ++ query

        request : Http.Request SearchResult  
        request = Http.get url Decode.string
    in
        Http.send ReceivedResponse request            


view model =
    div [] 
        [ Html.input [onInput (\text -> NewSearchText text)] []
        , Html.text model.result
        ]

Upvotes: 7

Views: 250

Answers (2)

Dogbert
Dogbert

Reputation: 222198

Here's one way:

Add two integers to your model:

  1. requestsSent : Int -- the number of requests made.
  2. lastReceived : Int -- the latest request that you've processed.

Modify ReceivedResponse to have an Int as the first value:

| ReceivedResponse Int (Result Http.Error SearchResult)

Now, whenever you make a request, increment requestsSent by 1 in the model and "tag" the request by partially applying ReceivedResponse:

Http.send (ReceivedResponse model.requestsSent) request

In your update function, check if the Int in the ReceivedResponse is greater than lastReceived or not. If it is, process it, and set the value of lastReceived to this response's Int. If it isn't, discard it, because you've already processed a newer request.

Upvotes: 7

Alex Lew
Alex Lew

Reputation: 2124

Yes, it is possible to attach the query string to the response. First, augment your message type to handle the additional data:

type Msg 
  = NewSearchText String
  | ReceivedResponse String (Result Http.Error SearchResult)

Then, change your Http.send call to attach the query text to the ReceivedResponse message:

Http.send (ReceivedResponse query) request

Finally, in your update, grab the query in your pattern match on the resulting Msg:

case msg of
  ReceivedResponse query (Ok response) ->
    ...
  ReceivedResponse query (Err err) ->
    ...

Why does this work?

The Http.send function's first argument can be an arbitrary function that consumes a Result Http.Error SearchResult and turns it into a Msg. In your original code, that function is just ReceivedResponse, the Msg constructor. When the Msg type is updated so that ReceivedResponse takes two arguments, the ReceivedResponse constructor function becomes a curried two-argument function, and ReceivedResponse "some query here" is a one-argument function that takes in a Result and returns a Msg.

Upvotes: 7

Related Questions