Add Swift 5.10+ onMainActor route builder#218
Open
louisdebaere wants to merge 6 commits intospotify:masterfrom
Open
Add Swift 5.10+ onMainActor route builder#218louisdebaere wants to merge 6 commits intospotify:masterfrom
louisdebaere wants to merge 6 commits intospotify:masterfrom
Conversation
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.
Why make this change
Mobius supports routing effects to the main thread via
.on(queue: .main), but when the handler touches@MainActor-isolated state, Swift requires the call site to manually bridge isolation — typically withMainActor.assumeIsolated. This scatters a framework-level concern across consumer code and creates repetitive boilerplate that's easy to get wrong.As projects adopt Swift Concurrency and
@MainActorannotations spread (especially with Swift 6.2'sdefaultIsolation(MainActor.self)), this friction multiplies — every main-queue effect route needs the same wrapper.This change introduces a Swift Concurrency-aware route that centralises the isolation bridge inside Mobius, keeping consumer code clean while preserving the existing dispatch semantics.
What will change
_PartialEffectRouter.onMainActor()returning a dedicated_MainActorPartialEffectRouter..to(...)overloads on_MainActorPartialEffectRouteraccepting@MainActor @Sendableclosures:(EffectParameters) -> Void() -> Void(whenEffectParameters == Void).on(queue: .main)route path, then invoke closures viaMainActor.assumeIsolated— no new dispatch mechanism, noTask, no ordering changes.#if compiler(>=5.10).Before / After
Manual bridge at every call site:
Bridge handled by the framework:
Existing queue-based routes are completely unaffected:
On compilers older than 5.10,
.onMainActor()is unavailable by design. The existing.on(queue: .main).to { MainActor.assumeIsolated { … } }pattern remains the compatibility path.Why this shape
Queue dispatch and actor isolation are separate concepts.
.on(queue:)is a GCD concern;.onMainActor()is a Swift Concurrency concern. Giving them distinct API surfaces makes intent clear and avoids conflating the two models.No new dispatch path.
.onMainActor()delegates to the same.on(queue: .main)routing that already exists. Dispatch order, callback lifecycle, andDisposablesemantics are identical — the only addition is theassumeIsolatedbridge.Centralised bridge.
MainActor.assumeIsolatedmoves from potentially hundreds of call sites into one place in the framework, where it's structurally correct (the main queue is the MainActor executor) and documented.Ready for Swift 6.2 consumer settings. When consumers enable
defaultIsolation(MainActor.self)or Approachable Concurrency (NonisolatedNonsendingByDefault), their closures are implicitly@MainActor— the compiler picks the.onMainActor().to { }overload without any annotation at the call site. Without these overloads, those same consumers would face type mismatches or be forced to addnonisolatedworkarounds to use the existing queue-based API.Alternatives considered
@MainActoroverloads on the queue-agnostic builder. Simpler, but permits.on(queue: .global()).to(@MainActor ...)— shifts misconfiguration from compile time to a runtime trap.Task { @MainActor in }instead ofassumeIsolated. Safe by construction, but introduces an async hop on top of the queue dispatch, changing ordering guarantees and complicatingcallback.end()placement.@_disfavoredOverload. Compile-time enforcement, but relies on underscored API, breaks when aliasingDispatchQueue.main, and would strand_PartialEffectRouterextensions that return queue-bound builders.@MainActoradoption grows.Given these trade-offs,
.onMainActor().to { }is the minimal, explicit, and forwards-compatible addition.