lobanov/elm-taskport/1.2.1

NOTE: the latest version is 2.0.1

TaskPort

This module allows to invoke JavaScript functions using the Elm's Task abstraction, which is convenient for chaining multiple API calls without introducing the complexity in the model of an Elm application.

Setting up

Before TypePort can be used in Elm, it must be set up on JavaScript side. Refer to the README for comprehensive instructions.

Usage

String
-> Decoder body
-> Decoder error
-> (args -> Value)
-> args
-> Task (Error error) body

Creates a Task encapsulating an invocation of a particular asyncronous JavaScript function. This function will usually be wrapped into a more specific one, which will partially apply it providing the encoder and the decoders but curry the last parameter, so that it could invoked where necessary as a args -> Task function.

Because interop calls can fail, produced task would likely need to be piped into a Task.attempt or handled further using Task.onError.

Here is a simple example that creates a Cmd invoking a registered JavaScript function called ping and produces a message GotPong with a Result, containing either an Ok variant with a string (determined by the first decoder argument), or an Err, containing a TaskPort.Error describing what went wrong.

type Msg = GotWidgetName (Result String String)


TaskPort.call "getWidgetNameByIndex" Json.Decode.string TaskPort.jsErrorDecoder Json.Encode.int 0
    |> Task.attempt GotWidgetName

The Task abstraction allows to effectively compose chains of tasks without creating many intermediate variants in the Msg type, and designing the model to deal with partially completed call chain. The following example shows how this might be used when working with a hypothetical 'chatty' JavaScript API, requiring to call getWidgetsCount function to obtain a number of widgets, and then call getWidgetName with each widget's index to obtain its name.

type Msg = GotWidgets (Result String (List String))


TaskPort.callNoArgs "getWidgetsCount" Json.Decode.int TaskPort.jsErrorDecoder
    |> Task.andThen
        (\count ->
            List.range 0 (count - 1)
                |> List.map Json.Encode.int
                |> List.map (TaskPort.call "getWidgetNameByIndex" TaskPort.jsErrorDecoder Json.Decode.string)
                |> Task.sequence
        )
    |> Task.attempt GotWidgets

The resulting task has type Task (TaskPort.Error String) (List String), which could be attempted as a single command, which, if successful, provides a handy List String with all widget names.

callNoArgs : String -> Decoder body -> Decoder error -> Task (Error error) body

Special version of the call that reduces amount of boilerplate code required when calling JavaScript functions that don't take any parameters. It is eqivalent of passing Json.Encoder.null into the call.

type Msg = GotWidgetsCount (Result String Int)


TaskPort.callNoArgs "getWidgetsCount" Json.Decode.int Json.Decode.string
    |> Task.attempt GotWidgetsCount

Error handling

type Error x
= InteropError InteropError
| CallError x

A structured error describing exactly how the interop call failed. You can use this to determine the best way to react to and recover from the problem.

CallError prefix is for errors explicitly sent from the JavaScript side. The error information will be specific to the interop use case, and it should be reconsituted from a JSON payload.

InteropError prefix is for the failures of the interop mechanism itself.

type JSError
= ErrorObject String JSErrorRecord
| ErrorValue Value

Generic type representing all possibilities that could be returned from an interop call. JavaScript is very lenient regarding its errors. Any value could be thrown, and, if the JS code is asynchronous, the Promise can reject with any value. TaskPort always attempts to decode erroneous results returned from iterop calls using ErrorObject variant and JSErrorRecord structure, which contains standard fields for JavaScript Error object, but if itsn't possible, it will resorts to ErrorValue variant followed by the JSON value as-is.

In most cases you would pass values of this type to jsErrorToString to create a useful diagnostic information, but you might also have a need to handle certain types of errors in a particular way. To make that easier, ErrorObject variant lifts up the error name to aid pattern-match for error types. You may do something like this:

case error of
    CallError (ErrorObject "VerySpecificError" _) -> -- handle a particular subtype of Error thrown by the JS code
    _ -> -- respond to the error in a generic way, e.g show a diagnostic message
type alias JSErrorRecord =
{ name : String
, message : String
, stackLines : List String
, cause : Maybe JSError
}

Structure describing an object conforming to JavaScript standard for Error. Unless you need to handle very specific failure condition in a particular way, you are unlikely to use this type.

The structure contains the following fields:

  • name represents the type of the Error object, e.g. ReferenceError
  • message is a free-form string typically passed as a parameter to the error constructor
  • stackLines is a platform-specific stack trace for the error
  • cause optional nested error object, which is first attempted to be decoded as a JSErrorRecord, but falls back to JSError.ErrorValue if that's impossible.
= FunctionNotFound
| NotInstalled
| VersionMismatch
| CannotDecodeResponse Error String
| CannotDecodeError Error String
| RuntimeError String

Subcategory of errors indicating a failure of the interop mechanism itself. These errors are generally not receoverable, but you can use them to allow the application to fail gracefully, or at least provide useful context for debugging, for which you can use helper function interopErrorToString.

JSON decoder that constructs a JSError value from erroneous results returned by an interop call. You would pass it to TaskPort.call or TaskPort.callNoArgs.

In most cases instances of InteropError indicate a catastrophic failure in the application environment and thus cannot be recovered from. This function allows Elm application to fail gracefully by displaying an error message to the user, that would help application developer to debug the issue.

It produces multiple lines of output, so you may want to peek at it with something like this:

import Html


errorToHtml : TaskPort.Error -> Html.Html msg
errorToHtml error =
  Html.pre [] [ Html.text (TaskPort.interopErrorToString error) ]

Generates a human-readable and hopefully helpful string with diagnostic information describing a JSError. It produces multiple lines of output, so you may want to peek at it with something like this:

import Html


errorToHtml : TaskPort.JSError -> Html.Html msg
errorToHtml error =
  Html.pre [] [ Html.text (TaskPort.jsErrorToString error) ]

Convenience method for creating a user-presentable string describing an error which occured during an interop call in case use jsErrorDecoder in your interop calls.

Tests

We are exposing tests suite to help test module's implementation details.

tests : Test

This is an embedded tests suite allowing to test module-internal logic. No need to use this.