lobanov / elm-taskport / 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


type alias FunctionName =
String

Alias for String type representing a name of a JavaScript function. Valid function names only contain alphanumeric characters.

call : { function : FunctionName, valueDecoder : Json.Decode.Decoder value, argsEncoder : args -> Json.Encode.Value } -> args -> Task value

Creates a Task encapsulating an asyncronous invocation of a particular 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 (TaskPort.Result String)

TaskPort.call
    { function = "getWidgetNameByIndex"
    , valueDecoder = Json.Decode.string
    , argsEncoder = 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 (List String))

getWidgetsCount : TaskPort.Task Int
getWidgetsCount = TaskPort.callNoArgs 
    { function = "getWidgetsCount"
    , valueDecoder = Json.Decode.int
    }

getWidgetNameByIndex : Int -> TaskPort.Task String
getWidgetNameByIndex = TaskPort.call
    { function = "getWidgetNameByIndex"
    , valueDecoder = Json.Decode.string
    , argsEncoder = Json.Encode.int
    } -- notice currying to return a function taking Int and producing a Task

getWidgetsCount
    |> Task.andThen
        (\count ->
            List.range 0 (count - 1)
                |> List.map getWidgetNameByIndex
                |> Task.sequence
        )
    |> Task.attempt GotWidgets

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

callNoArgs : { function : FunctionName, valueDecoder : Json.Decode.Decoder value } -> Task value

Special version of the call that reduces amount of boilerplate code required when calling JavaScript functions that don't take any parameters.

type Msg = GotWidgetsCount (TaskPort.Result Int)

TaskPort.callNoArgs
    { function = "getWidgetsCount"
    , valueDecoder = Json.Decode.int
    }
      |> Task.attempt GotWidgetsCount

ignoreValue : Json.Decode.Decoder ()

JSON decoder that can be used with as a valueDecoder parameter when calling JavaScript functions that are not expected to return a value, or where the return value can be safely ignored.

Error handling


type Error
    = InteropError InteropError
    | JSError JSError

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.

JSError variant 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 variant is for the failures of the interop mechanism itself.


type alias Result value =
Result Error value

Convenience alias for a Result obtained from passing a Task created by one of the variants of the TaskPort.call function to Task.attempt'. Application code may be simplified, because TaskPort always usesTaskPort.ErrorforResult.Err`.

type Msg = GotResult TaskPort.Result String

Task.attempt GotResult TaskPort.call { {- ... call details ... -} } args

Writing TaskPort.Result value is equivalent to writing Result TaskPort.Error value.


type alias Task value =
Task Error value

Convenience alias for a Taskcreated by one of the variants of the TaskPort.call function. Application code may be simplified, because TaskPort always uses TaskPort.Error for the error parameter of the Tasks it creates.

callJSFunction : String -> TaskPort.Task String
callJSFunction arg = TaskPort.call { {- ... call details ... -} } arg

Writing TaskPort.Task value is equivalent to writing Task TaskPort.Error value.


type JSError
    = ErrorObject String JSErrorRecord
    | ErrorValue Json.Encode.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 followed by JSErrorRecord structure, which contains standard fields for JavaScript Error object, but if that isn't possible, it resorts to ErrorValue variant followed by the JSON value as-is.

In most cases you would pass values of this type to errorToString 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
    JSError (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 the Error object. Unless you need to handle very specific failure condition in a particular way, you are unlikely to use this type directly.

The structure contains the following fields: name represents the type of the Error object, e.g. ReferenceError message is a free-form and potentially empty string typically passed as a parameter to the error constructor stackLines is a platform-specific stack trace for the error cause is an optional nested error object, which is first attempted to be decoded as a JSErrorRecord, but falls back to JSError.ErrorValue if that's not possible.


type InteropError
    = NotInstalled
    | NotFound String
    | NotCompatible String
    | CannotDecodeValue Json.Decode.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.

Interop calls can fail for various reasons: NotInstalled: JavaScript companion code responsible for TaskPort operations is missing or not working correctly, which means that no further interop calls can succeed. NotFound: TaskPort was unable to find a registered function name, which means that no further calls to that function can succeed. String value will contain the function name. NotCompatible: JavaScript and Elm code are not compatible. String value will contain the function name. CannotDecodeValue: value returned by the JavaScript function cannot be decoded with a given JSON decoder. String value will contain the returned value verbatim, and Json.Decode.Error will contain the error details. * RuntimeError: some other unexpected failure of the interop mechanism. String value will contain further details of the error.

interopErrorToString : InteropError -> String

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) ]

errorToString : Error -> String

Generates a human-readable and hopefully helpful string with diagnostic information describing an error. 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) ]

Package development

Make sure you read section on package development in the README.


type QualifiedName

Represents the name of a function that may optionally be qualified with a versioned namespace.


type alias Namespace =
String

Alias for String type representing a namespace for JavaScript interop functions. Namespaces are typically used by Elm package developers, and passed as a paramter to QualifiedName. Valid namespace string would match the following regular expression: `/^[\w-]+\/[\w-]+$/.

The following are valid namespaces: elm/core, lobanov/elm-taskport, rtfeldman/elm-iso8601-date-strings.


type alias Version =
String

Alias for String type representing a version of a namespace for JavaScript interop functions. Namespaces are typically used by Elm package developers, and passed as a parameter to QualifiedName. TaskPort does not enforce any versioning scheme and allows any combination of alphanumeric characters, dots, and dashes. Most likely, Elm package developers will use Elm package version.

noNamespace : FunctionName -> QualifiedName

Constructs a QualifiedName for a function in the default namespace. It's better to use non-namespace-aware call or callNoArgs function, but it's provided for completeness.

inNamespace : Namespace -> Version -> FunctionName -> QualifiedName

Constructs a QualifiedName for a function in a particular versioned namespace.

"functionName" |> inNamespace "author/package" "version" -- infix notation reads better...
inNamespace "author/package" "version" "functionName" -- ... but this also works

callNS : { function : QualifiedName, valueDecoder : Json.Decode.Decoder value, argsEncoder : args -> Json.Encode.Value } -> args -> Task value

Creates a Task encapsulating an asyncronous invocation of a particular JavaScript function. It behaves similarly to call, but this function is namespace-aware and is intended to be used by Elm package developers, who want to use TaskPort's function namespaces feature to eliminate a possibility of name clashes of their JavaScript functions with other packages that may also be using taskports.

Unlike call, this function uses a record to specify the details of the interop call, which leads to more readable code.

TaskPort.callNS
    { function = TaskPort.WithNS "elm-package/namespace" "1.0.0" "setWidgetName"
    , valueDecoder = TaskPort.ignoreValue -- expecting no return value
    , argsEncoder = Json.Encoder.string
    }
    "new name"
        |> Task.attempt WidgetNameUpdated

callNoArgsNS : { function : QualifiedName, valueDecoder : Json.Decode.Decoder value } -> Task value

Creates a Task encapsulating an asyncronous invocation of a particular JavaScript function without parameters. It behaves similarly to callNoArgs, but this function is namespace-aware and is intended to be used by Elm package developers, who want to use TaskPort's function namespaces feature to eliminate a possibility of name clashes of their JavaScript functions with other packages that may also be using taskports.

Unlike callNoArgs, this function uses a record to specify the details of the interop call, which leads to more readable code.

TaskPort.callNoArgsNS
    { function = TaskPort.WithNS "elm-package/namespace" "1.0.0" "getWidgetName"
    , valueDecoder = Json.Decoder.string -- expecting a string
    }
        |> Task.attempt GotWidgetName