Avoid Http Race Condition in Elm - 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
]

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.

Here's one way:
Add two integers to your model:
requestsSent : Int -- the number of requests made.
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.

Related

Elm: How to use data from one HTTP request in subsequent requests

I am new to Elm and just read the docs (https://guide.elm-lang.org/). I am modifying an example from there and playing around.
What I want to do is to hit an endpoint which will give me a list of IDs. Later I want to hit another endpoint with each of these IDs and display the results.
https://hacker-news.firebaseio.com/v0/topstories.json -
This endpoint has a list of IDs.
https://hacker-news.firebaseio.com/v0/item/[ID].json -
This endpoint will give the details of the story of given ID.
With what I have till now, I can get the list of all IDs separately and I can get each story separately (hard-coded ID) and display them. But what I am trying achieve here is to
get the list of IDs (500 of them) from endpoint 1
get first 5 of the stories by hitting endpoint 2
have a "load more" button which will load 5 more and so on
I am not sure how to do this. Any help is greatly appreciated.
Thanks
You can fire the second request when you handle the response from the first endpoint. Something like:
type Msg
= GotIds (Result Http.Error (List Int))
| GotStory (Result Http.Error (String))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotIds result ->
case result of
Ok (first::rest) ->
({ model | ids = first::rest }, getStory first)
Ok _ ->
(model, Cmd.none)
Err _ ->
({ model | story = "ERROR"}, Cmd.none)
GotStory result ->
({model | story = Result.withDefault "None" result}, Cmd.none)
If you want to fire multiple Cmd at the same time, you can use Cmd.batch
Here is an Ellie that gets the ids from the first request and then fetches the title for the first ID.
You will want to create a custom type and decoder for each post.
For posterity's sake, here is all of the code from the Ellie:
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
import Http
import Json.Decode exposing (Decoder, field, int, list, string )
type alias Model =
{ ids : List Int
, story : String
}
initialModel : Model
initialModel =
{ ids = []
, story = "None"
}
type Msg
= GotIds (Result Http.Error (List Int))
| GotStory (Result Http.Error (String))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotIds result ->
case result of
Ok (first::rest) ->
({ model | ids = first::rest }, getStory first)
Ok [] ->
(model, Cmd.none)
Err _ ->
({ model | story = "ERROR"}, Cmd.none)
GotStory result ->
({model | story = Result.withDefault "None" result}, Cmd.none)
view : Model -> Html Msg
view model =
div []
[ text model.story
]
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : () -> (Model, Cmd Msg)
init flags =
(initialModel, getIds)
getIds : Cmd Msg
getIds =
Http.get
{ url = "https://hacker-news.firebaseio.com/v0/topstories.json"
, expect = Http.expectJson GotIds (list int)
}
getStory : Int -> Cmd Msg
getStory id =
Http.get
{ url = "https://hacker-news.firebaseio.com/v0/item/" ++ String.fromInt id ++ ".json"
, expect = Http.expectJson GotStory (field "title" string)
}

Elm Http.post needs to produces Http.Request Int but send needs Http.Request String

I decode response from Http.post request, where return type is Integer:
responseDecoder : Decoder Int
responseDecoder =
field "data" (field "createDeadline" (field "id" int))
My problem is that I use Http.send which needs String:
createDeadline value =
Http.send Resolved (Http.post deadlineUrl (encodeBody value |> Http.jsonBody) responseDecoder)
And I dont know how to change the return type. My error message is following:
The 2nd argument to `send` is not what I expect:
113| Http.send Resolved (Http.post deadlineUrl (encodeBody value |> Http.jsonBody) responseDecoder)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This `post` call produces:
Http.Request Int
But `send` needs the 2nd argument to be:
Http.Request String
Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!
Hint: Want to convert an Int into a String? Use the String.fromInt function!
Can anyone help with this? I'm just playing around with Elm, but I got stuck here.
My problem is that I use Http.send which needs String
According to the docs, send function requires the argument to be of type Request a, where a can be any type (Int or String or something else).
The problem that you have is just what the hint in the compile error says:
Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!
Thus, it seems that you've already defined somewhere, that you're expecting String and the compiler infered the type to Request String. For example, you might have the Resolved defined something like the following:
type Msg = Resolved (Result Http.Error String)
And the compiler inferred the polymorphic type of send : (Result Error a -> msg) -> Request a -> Cmd msg to something specific, because it already sees that the first argument is or type (Result Error String -> msg):
send : (Result Error String -> msg) -> Request String -> Cmd msg
So, in this case the solution is either to change the expecting type:
type Msg = Resolved (Result Http.Error Int)
Or change the decoder and decode the response to String:
responseDecoder : Decoder String
responseDecoder =
Json.Decode.map String.fromInt (field "data" (field "createDeadline" (field "id" int)))

Command not being invoked after calling update

Building on the Elm navigation tutorial, I needed to execute a command to fetch additional data once navigating to my CategoryRoute.
My View.elm looks something like this:
view : Model -> Html Msg
view model =
div []
[ page model ]
page : Model -> Html Msg
page model =
case model.categories of
RemoteData.Success categories ->
case model.currentRoute of
CategoryListRoute ->
CategoryList.view categories
CategoryRoute id ->
let maybeCategory =
categories
|> SubCategories
|> flatten
|> filter (\category -> category.id == id)
|> head
_ = update (OnCategorySelected id) model
in
case maybeCategory of
Just category ->
Category.view category
Nothing ->
notFound
You'll notice that I'm calling update with the OnCategorySelected message myself when the currentRoute changes to the CategoryRoute.
My Update.eml looks something like this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnArticlesFetch response ->
let
_ = log "got response" response
in
( { model | articles = response }, Cmd.none)
OnLocationChange location ->
let
newRoute =
parseLocation location
in
( { model | currentRoute = newRoute }, Cmd.none )
OnCategorySelected id ->
( model, (getArticles model.tenant id) )
And finally, my Commands.eml looks like so:
getArticles : String -> String -> Cmd Msg
getArticles tenant id =
let
url =
"https://" ++ tenant ++ ".my.api"
_ = log "getArticles for " id
in
Http.post url (Http.jsonBody (encoder id)) decoder
|> RemoteData.sendRequest
|> Cmd.map OnArticlesFetch
I was expecting that once I'll call update OnCategorySelected, it will in turn invoke the getArticles function, which is passed a Cmd Msg, which I had thought will be invoked once the response comes in.
The problem I'm facing is that while update OnCategorySelected and getArticles seem to get invoked (as indicated by the log printouts log "getArticles for " id), I'm seeing no outgoing HTTP calls, no errors, no results and no log "got response" response printouts.
I'm confused as to what am I doing wrong here and what's the pattern for actually fetching more data as one navigates to a page in Elm...
Elm is a pure language where side effects are relegated to the framework. Calling the update function does not actually perform any work itself. It simply returns a value that can be handed off to the Elm framework directing it to interact with the outside world. That means when you call update from within the page function and discard the result, nothing happens.
One thing that can cause confusion is that Debug.log actually does get called and prints to the console, which violates the aforementioned purity of the language. It's just a magic function which exists only for debugging so hopefully it doesn't cause too much confusion.
You should instead be handling the RemoteData.Success case in the update function after parsing the route in the OnLocationChange case and returning a result which includes the getArticles result Cmd.
OnLocationChange location ->
let
newRoute =
parseLocation location
cmd =
case newRoute of
CategoryRoute id ->
getArticles model.tenant id
_ ->
Cmd.none
in
( { model | currentRoute = newRoute }, cmd )

how to create dynamic http body in elm

I am implementing the service where i have to create the dynamic http post request and my code is below.
postRequest: Int -> Http.Request
postRequest catId =
let
body =
"""{"categoryId:"""++catId++""","coupon":false,"domainId":1,"locations":[],"onlineMenu":false,"onlineOrder":false,"pageNo":1,"pageSize":10,"reservation":false,"searchText":"","subcategories":[]}"""
in
{ verb = "POST"
, headers =
[("Content-Type", "application/json")
]
, url = "http://xyz/businesses/list"
, body = Http.string body
}
but i am getting some error
how to concatenate the catId in the body and catId is integer type.
please anyone suggest what i have did wrong in the implementation.
As you declared catId as Int not String, so
(++) : String -> String -> String cannot not work on it.
You can use toString : a -> String before concatenating it with strings.
"categoryId:" ++ (toString catId)

How to convert Task Http.RawError Http.Response to Task String (Int, Int)

There are a type and a task
type Msg
= Fail String
| Success (Int, Int)
makeRequest =
let
req =
{ verb = "GET"
, headers = []
, url = "http://localhost:8080"
, body = empty
}
in
Task.perform Fail Success <| send defaultSettings req
The argument of Fail constructor is for error message (just "Error"), first argument of Succeess is for status from Http.Response, second is for size of value from Http.Response.
How to convert Task Http.RawError Http.Response to Task String (Int, Int)?
I'm looking at Task.map and Tsk.mapError and I don't understand how to combine them. Am I on a right way?
Yes, you can use Task.map and Task.mapError to achieve your results.
First off, you'll need a way to determine the size of your Http Response. Since it can be either a string or binary blob, and blob is not yet supported, you could define a function like this:
httpValueSize : Http.Value -> Int
httpValueSize val =
case val of
Text str -> String.length str
Blob blob -> Debug.crash "Blobs have no implementation yet"
Now you can use the mapping functions in your task like this:
send defaultSettings req
|> Task.map (\r -> (r.status, httpValueSize r.value))
|> Task.mapError (always "Error")
|> Task.perform Fail Success
You could also do this without the mapping functions like so:
send defaultSettings req
|> Task.perform (always <| Fail "Error") (\r -> Success (r.status, httpValueSize r.value))