Task api - run a tree of tasks concurrently.Task Ports - call JavaScript functions as tasks.This package is heavily inspired by elm-pages' BackendTask and is intended to be a standalone implementation that can be dropped into any Elm app - big kudos to Dillon for the idea.
See the examples for more things you can do!
View the elm-package docs here.
Task.map2, Task.map3+ In elm/core run each subtask in sequence.
Whilst it's possible to run these subtasks concurrently as separate Cmds, it can be a lot of wiring and boilerplate, including:
Elm Task Parallel handles this nicely but only at the top level (sub tasks cannot be parallelised).
And what if you want to speed up a complex nested task like:
Task.map2 combine
(task1
|> Task.andThen task2
|> Task.andThen
(\res ->
Task.map2 combine
(task3 res)
task4
)
)
(Task.map2 combine
task5
task6
)This is the elm equivalent of "callback hell".
This library helps you do this with a lot less boilerplate.
Sometimes you want to call JavaScript from elm in order. For example sequencing updates to localstorage:
NOTE: See a full working localstorage example here.
import ConcurrentTask exposing (ConcurrentTask)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
-- Preferences
type alias Preferences =
{ contrast : Int
, brightness : Int
}
setContrast : Int -> ConcurrentTask Error ()
setContrast contrast =
getItem "preferences" decodePreferences
|> ConcurrentTask.map (\preferences -> { preferences | contrast = contrast })
|> ConcurrentTask.andThen (encodePreferences >> setItem "preferences")
encodePreferences : Preferences -> Encode.Value
encodePreferences p =
Encode.object
[ ( "contrast", Encode.int p.contrast )
, ( "brightness", Encode.int p.brightness )
]
decodePreferences : Decoder Preferences
decodePreferences =
Decode.map2 Preferences
(Decode.field "contrast" Decode.int)
(Decode.field "brightness" Decode.int)
-- Localstorage
type Error
= NoValue
| ReadBlocked
| DecodeError Decode.Error
| WriteError String
getItem : String -> Decoder a -> ConcurrentTask Error a
getItem key decoder =
ConcurrentTask.define
{ function = "localstorage:getItem"
, expect = ConcurrentTask.expectString
, errors = ConcurrentTask.expectErrors decodeReadErrors
, args = Encode.object [ ( "key", Encode.string key ) ]
}
|> ConcurrentTask.map (Decode.decodeString decoder >> Result.mapError DecodeError)
|> ConcurrentTask.andThen ConcurrentTask.fromResult
setItem : String -> Encode.Value -> ConcurrentTask Error ()
setItem key value =
ConcurrentTask.define
{ function = "localstorage:setItem"
, expect = ConcurrentTask.expectWhatever
, errors = ConcurrentTask.expectThrows WriteError
, args =
Encode.object
[ ( "key", Encode.string key )
, ( "value", Encode.string (Encode.encode 0 value) )
]
}
decodeReadErrors : Decoder Error
decodeReadErrors =
Decode.string
|> Decode.andThen
(\reason ->
case reason of
"NO_VALUE" ->
Decode.succeed NoValue
"READ_BLOCKED" ->
Decode.succeed ReadBlocked
_ ->
Decode.fail ("Unrecognized Read Error: " ++ reason)
)Other implementations of Task Ports rely on either:
ServiceWorkers - intercept certain http requests and call custom JavaScript from the service worker.XMLHttpRequest - Modify methods on the global XMLHttpRequest to intercept http requests and call custom JavaScript.Both methods are not ideal (modifying global methods is pretty dodgy), and neither are portable to other environments like node (ServiceWorker and XMLHttpRequest are only native in the browser and require pollyfills).
elm-concurrent-task uses plain ports and a bit of wiring to create a nice Task api.
This makes it dependency free - so more portable (🤓) and less likely to break (😄).
Because elm-concurrent-task uses a different type to elm/core Task it's unfortunately not compatible with elm/core Tasks.
However, there are a number of tasks built into the JavaScript runner and supporting modules that should cover a large amount of the existing functionality of elm/core Tasks.
Check out the built-ins for more details:
Install the elm package with
elm install andrewMacmurray/elm-concurrent-taskInstall the JavaScript/TypeScript runner with
npm install @andrewmacmurray/elm-concurrent-taskYour Elm program needs:
A single ConcurrentTask.Pool in your Model to keep track of each task attempt:
type alias Model =
{ tasks : ConcurrentTask.Pool Msg
}2 Msgs to handle task updates:
type Msg
= OnProgress ( ConcurrentTask.Pool Msg, Cmd Msg ) -- updates task progress
| OnComplete (ConcurrentTask.Response Error Success) -- called when a task completes2 ports with the following signatures:
port send : Decode.Value -> Cmd msg
port receive : (Decode.Value -> msg) -> Sub msgHere's a simple complete program that fetches 3 resources concurrently:
port module Example exposing (main)
import ConcurrentTask exposing (ConcurrentTask)
import ConcurrentTask.Http as Http
import Json.Decode as Decode
type alias Model =
{ tasks : ConcurrentTask.Pool Msg
}
type Msg
= OnProgress ( ConcurrentTask.Pool Msg, Cmd Msg )
| OnComplete (ConcurrentTask.Response Error Titles)
type alias Error =
Http.Error
-- Get All Titles Task
type alias Titles =
{ todo : String
, post : String
, album : String
}
getAllTitles : ConcurrentTask Error Titles
getAllTitles =
ConcurrentTask.succeed Titles
|> ConcurrentTask.andMap (getTitle "/todos/1")
|> ConcurrentTask.andMap (getTitle "/posts/1")
|> ConcurrentTask.andMap (getTitle "/albums/1")
getTitle : String -> ConcurrentTask Error String
getTitle path =
Http.get
{ url = "https://jsonplaceholder.typicode.com" ++ path
, headers = []
, expect = Http.expectJson (Decode.field "title" Decode.string)
, timeout = Nothing
}
-- Program
init : ( Model, Cmd Msg )
init =
let
( tasks, cmd ) =
ConcurrentTask.attempt
{ send = send
, pool = ConcurrentTask.pool
, onComplete = OnComplete
}
getAllTitles
in
( { tasks = tasks }, cmd )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
OnComplete response ->
let
_ =
Debug.log "response" response
in
( model, Cmd.none )
OnProgress ( tasks, cmd ) ->
( { model | tasks = tasks }, cmd )
subscriptions : Model -> Sub Msg
subscriptions model =
ConcurrentTask.onProgress
{ send = send
, receive = receive
, onProgress = OnProgress
}
model.tasks
port send : Decode.Value -> Cmd msg
port receive : (Decode.Value -> msg) -> Sub msg
main : Program {} Model Msg
main =
Platform.worker
{ init = always init
, update = update
, subscriptions = subscriptions
}Connect the runner to your Elm app (the runner supports both import and require syntax):
import * as ConcurrentTask from "@andrewmacmurray/elm-concurrent-task";
const app = Elm.Main.init({});
ConcurrentTask.register({
tasks: {},
ports: {
send: app.ports.send,
receive: app.ports.receive,
},
});The value passed to tasks is an object of task names to functions (the functions can return plain synchronous values or promises)
e.g. tasks for reading and writing to localStorage:
const tasks = {
"localstorage:getItem": (args) => localStorage.getItem(args.key),
"localstorage:setItem": (args) => localStorage.setItem(args.key, args.item),
};NOTE: for a more complete localStorage integration with proper error handling check out the localstorage example.
For most cases a single ConcurrentTask.Pool is paired a send and receive port pair.
If you need multiple Pools subscribed to a port pair at the same time or need to continually instantiate new Pools (e.g. switching between pages), make sure each pool has a unique id with ConcurrentTask.withPoolId. Otherwise Pools may receive results for other pools which can lead to some confusing states!
Install Dependencies:
npm installRun the tests with:
npm testTo preview any changes, try some of the examples in the examples folder.
View the docs locally with:
npm run docselm bump to bump the elm version.version in package.json to match the new elm version.git tag -a 2.0.0 && git push origin 2.0.0npm run runner:compile && npm publishelm publish