Skip to content

Commit 7e3ff72

Browse files
Merge pull request #52 from andrewMacmurray/cancel-tasks
Allow running tasks to be cancelled
2 parents d14b845 + 91c8688 commit 7e3ff72

File tree

3 files changed

+111
-25
lines changed

3 files changed

+111
-25
lines changed

examples/pages-example/src/Main.elm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ update msg model =
164164
( model, Nav.load u )
165165

166166
OnUrlChange url ->
167-
{ model | tasks = ConcurrentTask.pool, globalError = Nothing }
167+
{ model | tasks = ConcurrentTask.cancelAll model.tasks, globalError = Nothing }
168168
|> initPage (Route.fromUrl url)
169169

170170

src/ConcurrentTask.elm

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ module ConcurrentTask exposing
99
, race
1010
, batch, sequence
1111
, map, andMap, map2, map3, map4, map5
12-
, attempt, attemptEach, Response(..), UnexpectedError(..), onProgress, Pool, pool, withPoolId
12+
, attempt, attemptWithId, attemptEach, Response(..), UnexpectedError(..), onProgress, Pool, pool, withPoolId
13+
, cancel, cancelAll
1314
)
1415

1516
{-| A Task similar to `elm/core`'s `Task` but:
@@ -194,7 +195,14 @@ Here's a minimal complete example:
194195
, subscriptions = subscriptions
195196
}
196197
197-
@docs attempt, attemptEach, Response, UnexpectedError, onProgress, Pool, pool, withPoolId
198+
@docs attempt, attemptWithId, attemptEach, Response, UnexpectedError, onProgress, Pool, pool, withPoolId
199+
200+
201+
# Cancel Tasks
202+
203+
These can be used to stop running tasks from outside the task chain.
204+
205+
@docs cancel, cancelAll
198206
199207
-}
200208

@@ -1093,7 +1101,38 @@ attempt :
10931101
}
10941102
-> ConcurrentTask x a
10951103
-> ( Pool msg, Cmd msg )
1096-
attempt config task =
1104+
attempt options task =
1105+
let
1106+
( _, p, cmd ) =
1107+
attemptWithId options task
1108+
in
1109+
( p, cmd )
1110+
1111+
1112+
{-| Start a `ConcurrentTask` identical to [attempt](ConcurrentTask#attempt) except it returns an additional `String` id that can be used to cancel the task.
1113+
1114+
Pass this `String` id to [cancel](ConcurrentTask#cancel) to stop the running task. Below is a contrived example but it would stop the task immediately:
1115+
1116+
let
1117+
( id, tasks, cmd ) =
1118+
ConcurrentTask.attemptWithId
1119+
{ send = send
1120+
, pool = model.pool
1121+
, onComplete = OnComplete
1122+
}
1123+
myTask
1124+
in
1125+
( { model | tasks = ConcurrentTask.cancel id tasks }, cmd )
1126+
1127+
-}
1128+
attemptWithId :
1129+
{ pool : Pool msg
1130+
, send : Decode.Value -> Cmd msg
1131+
, onComplete : Response x a -> msg
1132+
}
1133+
-> ConcurrentTask x a
1134+
-> ( String, Pool msg, Cmd msg )
1135+
attemptWithId config task =
10971136
let
10981137
mappedTask : ConcurrentTask msg msg
10991138
mappedTask =
@@ -1139,8 +1178,11 @@ attemptEach config taskList =
11391178
let
11401179
attemptAccum : ConcurrentTask x a -> ( Pool msg, List (Cmd msg) ) -> ( Pool msg, List (Cmd msg) )
11411180
attemptAccum task ( pool_, cmds_ ) =
1142-
attempt { config | pool = pool_ } task
1143-
|> Tuple.mapSecond (\cmd -> cmd :: cmds_)
1181+
let
1182+
( p, cmd ) =
1183+
attempt { config | pool = pool_ } task
1184+
in
1185+
( p, cmd :: cmds_ )
11441186
in
11451187
List.foldl attemptAccum ( config.pool, [] ) taskList
11461188
|> Tuple.mapSecond Cmd.batch
@@ -1266,21 +1308,48 @@ pool =
12661308
Internal.pool
12671309

12681310

1269-
{-| Add an id to a `Pool`.
1311+
{-| Add an id to a `Pool`. Use this if you're creating multiple new `Pool`s for a single pair of ports.
12701312
12711313
Why? Because Pools can be instantiated multiple times (think switching pages in a Single Page App),
12721314
without a unique identifier a ConcurrentTask Pool may end up receiving responses for a ConcurrentTask pool that was previously discarded.
12731315
12741316
One example is a user switching back and forth between two pages:
12751317
1276-
- Page one has a long running task on `init`
1277-
- The user switches to page 2, then switches back to page 1
1278-
- A new long running task is started
1279-
- But the Pool can receive the response from the first long running task (which is unexpected behaviour)
1318+
- Page one has a long running task on `init`
1319+
- The user switches to page 2, then switches back to page 1
1320+
- A new long running task is started
1321+
- But the Pool can receive the response from the first long running task (which is unexpected behaviour)
12801322
12811323
Adding a different id to the `Pool` allows these previous responses to be ignored.
12821324
12831325
-}
12841326
withPoolId : Int -> Pool msg -> Pool msg
12851327
withPoolId =
12861328
Internal.withPoolId
1329+
1330+
1331+
{-| Cancel a running task. Pass the `String` id returned from [attemptWithId](ConcurrentTask#attemptWithId) to cancel that specific task.
1332+
1333+
If the task has already completed this does nothing.
1334+
1335+
-}
1336+
cancel : String -> Pool msg -> Pool msg
1337+
cancel =
1338+
Internal.cancel
1339+
1340+
1341+
{-| Cancel all running tasks for the given `Pool`.
1342+
1343+
**Why use `cancelAll` instead of creating a new `ConcurrentTask.pool`?**
1344+
1345+
Internally, a `Pool` tracks an attempt id and associates a running task with it.
1346+
Calling `cancelAll` increments this attempt id, ensuring any results from previously
1347+
started (and potentially long-running) tasks are ignored.
1348+
1349+
Creating a new pool resets the attempt identifier, which can allow messages from an
1350+
old task to be delivered to the new pool, leading to a subtle race condition.
1351+
1352+
-}
1353+
cancelAll : Pool msg -> Pool msg
1354+
cancelAll =
1355+
Internal.cancelAll

src/ConcurrentTask/Internal.elm

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ module ConcurrentTask.Internal exposing
1212
, andMap
1313
, andThen
1414
, attempt
15+
, cancel
16+
, cancelAll
1517
, define
1618
, fail
1719
, fromResult
@@ -451,11 +453,17 @@ type alias OnProgress msg =
451453
}
452454

453455

454-
attempt : Attempt msg -> ConcurrentTask msg msg -> ( Pool msg, Cmd msg )
456+
attempt : Attempt msg -> ConcurrentTask msg msg -> ( AttemptId, Pool msg, Cmd msg )
455457
attempt attempt_ task =
458+
let
459+
id : AttemptId
460+
id =
461+
currentAttemptId attempt_.pool
462+
in
456463
case stepTask Dict.empty ( Ids.init, task ) of
457464
( _, Done res ) ->
458-
( attempt_.pool
465+
( id
466+
, attempt_.pool
459467
, sendResult attempt_.onComplete res
460468
)
461469

@@ -468,8 +476,9 @@ attempt attempt_ task =
468476
, onComplete = attempt_.onComplete
469477
}
470478
in
471-
( startAttempt progress attempt_.pool
472-
, attempt_.send (encodeDefinitions (currentAttemptId attempt_.pool) defs)
479+
( id
480+
, startAttempt id progress attempt_.pool
481+
, attempt_.send (encodeDefinitions id defs)
473482
)
474483

475484

@@ -753,16 +762,19 @@ withPoolId id =
753762
mapPool (\pool_ -> { pool_ | poolId = Just id })
754763

755764

756-
startAttempt : Progress msg -> Pool msg -> Pool msg
757-
startAttempt progress p =
758-
mapPool
759-
(\pool_ ->
760-
{ pool_
761-
| attempts = Dict.insert (currentAttemptId p) progress pool_.attempts
762-
, attemptIds = Ids.next pool_.attemptIds
763-
}
764-
)
765-
p
765+
startAttempt : AttemptId -> Progress msg -> Pool msg -> Pool msg
766+
startAttempt id progress =
767+
mapPool (\pool_ -> { pool_ | attempts = Dict.insert id progress pool_.attempts }) >> nextAttemptId
768+
769+
770+
cancelAll : Pool msg -> Pool msg
771+
cancelAll =
772+
mapPool (\pool_ -> { pool_ | attempts = Dict.empty }) >> nextAttemptId
773+
774+
775+
cancel : AttemptId -> Pool msg -> Pool msg
776+
cancel id =
777+
mapPool (\pool_ -> { pool_ | attempts = Dict.remove id pool_.attempts }) >> nextAttemptId
766778

767779

768780
currentAttemptId : Pool msg -> AttemptId
@@ -775,6 +787,11 @@ currentAttemptId (Pool pool_) =
775787
Ids.get pool_.attemptIds
776788

777789

790+
nextAttemptId : Pool msg -> Pool msg
791+
nextAttemptId =
792+
mapPool (\pool_ -> { pool_ | attemptIds = Ids.next pool_.attemptIds })
793+
794+
778795
updateProgressFor : AttemptId -> Progress msg -> Pool msg -> Pool msg
779796
updateProgressFor attemptId progress_ =
780797
mapPool (\pool_ -> { pool_ | attempts = Dict.update attemptId (Maybe.map (always progress_)) pool_.attempts })

0 commit comments

Comments
 (0)