Repository navigation
Conversation
SkipLib's Kotlin `Task.cancel()` only flags the task and runs the handlers registered with `withTaskCancellationHandler`; it never cancels the coroutine. Since skiptools#420 moved the bridged `task(id:)` from `LaunchedEffect` into a `Task`, the `invokeOnCancellation` it waits on never fires, so a superseded or disposed task ran to completion. Run the action, then wait for its completion under `withTaskCancellationHandler`. Registering after the action has installed its `onCancel` also covers a cancel that lands while the action runs: SkipLib then calls the handler at once. Either way it runs once. The task now calls the action it was created with. It used to read it through `rememberUpdatedState` only once its coroutine started, so a task that started after its id had changed again could run the next id's action. `List` and `ScrollView` launch a bridged `RefreshAction` in a Compose coroutine scope, whose cancellation is a real coroutine cancellation, so pull-to-refresh is not affected. A test pins that down.
Ceylo
marked this pull request as ready for review
October 2, 2026 17:30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
In a Fuse app, a
.task/.task(id:)is never cancelled when its id changes or its view leaves: the old Swift task runs to completion withTask.isCancelled == false. This PR cancels it in both cases, as on iOS.Since #420,
task(id:bridgedAction:)runs in a SkipLibTaskand waits oninvokeOnCancellation. But SkipLib'sTask.cancel()never cancels the coroutine; it only runs handlers registered withwithTaskCancellationHandler. So the action'sonCancelwas never called.The fix calls the action, then waits for its completion under
withTaskCancellationHandler. Registering after the action has installedonCancelalso covers a cancel that lands while the action is still starting: SkipLib then calls the handler at once. Either way it runs once. The task also now calls the action it was created with, rather than reading it throughrememberUpdatedStatewhen its coroutine starts.Pull-to-refresh is not affected:
ListandScrollViewlaunchRefreshActionin a Compose scope, whose cancellation is real. A test pins that down.Before / after
Three new Robolectric tests fail on
mainand pass here. Each also checks thatonCancelruns once.mainwithtestBridgedTaskCancelledWhenIdChangesThe task for the previous id was not cancelledtestBridgedTaskCancelledWhenViewLeavesThe task of the removed view was not cancelledtestBridgedTaskCancelledBeforeItsActionInstallsOnCancelThe task cancelled before its action installed onCancel was not cancelledShowcase in Fuse, with temporary log lines in two playgrounds:
Side effects
.taskin a Fuse app is now cancelled when its view leaves, as on iOS. Code relying on a task outliving its view relied on this bug.RefreshAction.run(completion:)and the Litetask(id:priority:)overload are unchanged.LaunchedEffectwithout the crash Cancel.tasktask on dispose, instead of crashing the app #420 fixed. I kept Cancel.tasktask on dispose, instead of crashing the app #420'sTaskmodel.Verified
swift test: 120 Swift tests, 57 skipped, 0 failures. JUnit: 164 tests, 159 passed, 5 skipped.main's file each new test fails as above; with the handler registered before the action, only the third fails.Skip Pull Request Checklist:
swift testWritten with Claude Code: the fix, the tests and this description. Every claim above comes from a run:
swift test, the mutation runs, and the Showcase in Fuse on an emulator before and after.