Skip to content

Instantly share code, notes, and snippets.

@maxhoffmann
Last active December 22, 2017 13:19
Show Gist options
  • Save maxhoffmann/9c0eb54cbd5e2120ff9ad50ff1491167 to your computer and use it in GitHub Desktop.
Save maxhoffmann/9c0eb54cbd5e2120ff9ad50ff1491167 to your computer and use it in GitHub Desktop.
Signed Api Request Effect Manager
effect module Api.Manager where { command = MyCmd } exposing (request)
import Task exposing (..)
import Http
import Process
import Json.Decode as Json exposing (field)
import Native.Api
import Json.Decode as Json
import Time exposing (Time)
-- COMMANDS
type MyCmd msg
= ApiRequest (String -> Task Never msg)
request : (Http.Error -> msg) -> (a -> msg) -> (String -> Task Http.Error a) -> Cmd msg
request onFail onSuccess request =
command <|
ApiRequest <|
request
>> Task.map onSuccess
>> Task.onError (succeed << onFail)
cmdMap : (msg -> msg_) -> MyCmd msg -> MyCmd msg_
cmdMap func (ApiRequest requestWithoutToken) =
ApiRequest (Task.map func << requestWithoutToken)
-- MANAGER
type alias State msg =
{ token : Token
, requestQueue : List (MyCmd msg)
, retries : Float
}
type Token
= Valid String Time
| Invalid
| Refreshing
init : Task Never (State msg)
init =
succeed (State Invalid [] 0)
-- HANDLE APP MESSAGES
onEffects :
Platform.Router msg Msg
-> List (MyCmd msg)
-> State msg
-> Task Never (State msg)
onEffects router requests ({ token, requestQueue } as state) =
let
updatedState =
{ state | requestQueue = List.append requests requestQueue }
in
case token of
Invalid ->
Process.spawn (Platform.sendToSelf router RefreshToken)
&> succeed updatedState
Refreshing ->
succeed updatedState
Valid secret expireTime ->
sendRequests router secret expireTime
&> succeed updatedState
sendRequests : Platform.Router msg Msg -> String -> Float -> Task Never Process.Id
sendRequests router token expireTime =
let
sendRequestsOrRefreshToken now =
if now >= expireTime then
Platform.sendToSelf router RefreshToken
else
Platform.sendToSelf router (SendRequests token)
in
Process.spawn (Time.now |> andThen sendRequestsOrRefreshToken)
-- HANDLE SELF MESSAGES
type Msg
= RefreshToken
| RefreshSuccess ( String, Time )
| RefreshFailure Http.Error
| SendRequests String
onSelfMsg : Platform.Router msg Msg -> Msg -> State msg -> Task Never (State msg)
onSelfMsg router selfMsg state =
case selfMsg of
RefreshToken ->
refreshToken router
&> succeed { state | token = Refreshing }
RefreshSuccess ( secret, expirationTime ) ->
Process.spawn (Platform.sendToSelf router (SendRequests secret))
&> succeed { state | token = Valid secret expirationTime, retries = 0 }
RefreshFailure error ->
case error of
Http.BadPayload _ _ ->
Process.spawn (redirect "/auth/sign_in")
&> succeed state
_ ->
if state.retries < 3 then
Process.spawn
(Process.sleep (state.retries * 5000)
&> Platform.sendToSelf router RefreshToken
)
&> succeed { state | retries = state.retries + 1 }
else
succeed state
SendRequests token ->
sequence (List.map (sendCmd router token) state.requestQueue)
&> succeed { state | requestQueue = [] }
refreshToken : Platform.Router msg Msg -> Task Never Process.Id
refreshToken router =
let
refreshSuccess tokenValues =
Platform.sendToSelf router (RefreshSuccess tokenValues)
refreshFailure error =
Platform.sendToSelf router (RefreshFailure error)
attemptRefresh =
map2 calculateExpireTime requestNewToken Time.now
|> andThen refreshSuccess
|> onError refreshFailure
in
Process.spawn attemptRefresh
calculateExpireTime : ( String, Time ) -> Time -> ( String, Time )
calculateExpireTime ( token, expires_in ) now =
( token, now + (expires_in * 1000) )
sendCmd : Platform.Router msg Msg -> String -> MyCmd msg -> Task Never Process.Id
sendCmd router token (ApiRequest requestWithoutToken) =
Process.spawn (requestWithoutToken token |> andThen (Platform.sendToApp router))
requestNewToken : Task Http.Error ( String, Time )
requestNewToken =
Http.request
{ method = "POST"
, headers =
[ (Http.header "Accept" "application/json")
, (Http.header "Content-Type" "application/json")
]
, url = "/auth/retrieve_token"
, body = Http.emptyBody
, expect =
Http.expectJson
(Json.map2 (,)
(field "access_token" Json.string)
(field "expires_in" Json.float)
)
, timeout = Nothing
, withCredentials = False
}
|> Http.toTask
-- Helpers
(&>) : Task a b -> Task a c -> Task a c
(&>) task1 task2 =
task1 |> andThen (\_ -> task2)
-- Native
redirect : String -> Task x ()
redirect path =
Native.Api.redirect path
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment