Chat Crash on Concurrency Fix#121
Merged
Bryan Malumphy (bmalumphy) merged 4 commits intomainfrom Apr 1, 2026
Merged
Conversation
Contributor
🟢 Test Coverage Report -
|
| Metric | Coverage | Status |
|---|---|---|
| 🟢 Lines | 86.6% | green |
| 🟢 Statements | 86.6% | green |
| 🟢 Functions | 92.45% | green |
| 🟢 Branches | 86.08% | green |
📊 View Detailed Coverage Report
ℹ️ Coverage Thresholds
- 🟢 Excellent (≥ 80%)
- 🟡 Good (60-79%)
- 🟠 Fair (40-59%)
- 🔴 Poor (< 40%)
Contributor
🟢 Test Coverage Report -
|
| Metric | Coverage | Status |
|---|---|---|
| 🟢 Lines | 91.94% | green |
| 🟢 Statements | 91.94% | green |
| 🟢 Functions | 86.11% | green |
| 🟢 Branches | 88.67% | green |
📊 View Detailed Coverage Report
ℹ️ Coverage Thresholds
- 🟢 Excellent (≥ 80%)
- 🟡 Good (60-79%)
- 🟠 Fair (40-59%)
- 🔴 Poor (< 40%)
Tom (tdwells90)
approved these changes
Apr 1, 2026
Erik Everson (ErikEverson)
approved these changes
Apr 1, 2026
Collaborator
Erik Everson (ErikEverson)
left a comment
There was a problem hiding this comment.
can not wait for v5 so we can drop all this stuff we have to do to work around it
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.
Fix Swift 6.3 runtime actor-isolation crashes (Xcode 26)
Xcode 26 / Swift 6.3 enforces strict actor isolation at runtime via injected
_swift_task_checkIsolatedSwiftchecks. Several patterns that compiled and rancorrectly under Swift 5.x now crash with
EXC_BREAKPOINTin_dispatch_assert_queue_failwhen those checks fire on the wrong thread. This PR fixes all four affected sites.
Root cause
Swift 6.3 injects an implicit
dispatch_assert_queuecheck:@MainActormethod, ifthat closure escapes to a non-
@Sendablecontext (e.g. a Ditto callback).@MainActor-isolated methods are invoked fromnonisolated Combine sinks, even when the sink is connected with
.receive(on: DispatchQueue.main)— GCD queues and the Swift concurrencymain actor executor are distinct; Ditto also ignores
deliverOn: .mainanddelivers on
utility-qosregardless.Changes
ChatNotificationManager.swiftProblem:
startObserving(room:)is@MainActor. Swift 6.3 injected anisolation check into the prologue of the
registerObserverclosure definedinside it. When Ditto delivered the callback on
utility-qos, that checkcrashed immediately — before any user code in the closure ran (frame shown as
ChatNotificationManager.swift:0in the backtrace).Fix:
import DittoSwift→@preconcurrency import DittoSwiftto suppresscompile-time sendability errors on the non-
@Sendablecallback type.registerObserverinto a newnonisolatedmethodmakeObserver(...).Closures defined in a
nonisolatedcontext carry no@MainActorcoloring, sono prologue check is injected.
makeObserver, usednonisolated(unsafe) weak var weakSelfto safelyreference
selfwithout actor-region taint, then hopped back to main viaDispatchQueue.main.async { MainActor.assumeIsolated { } }.DittoChat.swiftProblem: The Combine sink on
p2pStore.publicRoomsPublishercalledmanager?.syncRooms(rooms)— an@MainActormethod — from a nonisolated sinkusing
.receive(on: DispatchQueue.main). In Swift 6.3, this triggers a runtimeisolation check because the Swift concurrency main actor executor and
DispatchQueue.mainare distinct.Fix: Removed
.receive(on: DispatchQueue.main). Wrapped thesyncRoomscall in
Task { @MainActor [weak manager] in }so it enters@MainActorcontext through the Swift concurrency executor.
Publishers.swift—DittoStore.observePublisherProblem: Both
observePublisheroverloads sent values from the Dittocallback thread directly to downstream subscribers (e.g.
assign(to: \.allPublicRooms)in
DittoService,@Publishedproperties in ViewModels). DespitedeliverOn: .mainbeing passed to
registerObserver, Ditto delivers onutility-qos. Swift 6.3checks isolation at each
@MainActorproperty write, crashing when those writesarrived off-main.
Fix: Added
.receive(on: DispatchQueue.main)beforeeraseToAnyPublisher()in both overloads so all downstream subscribers are guaranteed to receive on main.
Concurrency.swift—asyncMapProblem:
value(of typeOutput) was captured by theFutureclosureand separately needed to be
sendingto theTaskclosure. Swift 6.3 regionisolation analysis flagged
valueas potentially aliased between the twoclosures. Additionally,
Future.Promiseis notSendable, causing asendingparameter error when captured by
Task.Fix:
@MainActorfrom bothasyncMapoverloads; addedOutput: Sendableconstraint on the extension and
T: Sendable+@Sendableon the transform.SendableBox<T>: @unchecked Sendableto wrap bothvalue(vBox)and
promise(pBox) inside eachFutureclosure, breaking the region-isolation alias analysis and satisfying the
sendingclosure requirement.Threading model after this PR
ChatNotificationManagerobserver callback@MainActor-colored closure, no hop → crashnonisolatedclosure →DispatchQueue.main.async→MainActor.assumeIsolatedDittoChatrooms sink.receive(on: DispatchQueue.main)+ nonisolated call → crashTask { @MainActor in }via Swift concurrency executorobservePublisherdownstream@MainActorwrites.receive(on: DispatchQueue.main)before subscribersasyncMapTask capturesSendableBoxwrappers bypass alias analysis