Skip to content

Chat Crash on Concurrency Fix#121

Merged
Bryan Malumphy (bmalumphy) merged 4 commits intomainfrom
bugfix/chat-crashes-on-observation
Apr 1, 2026
Merged

Chat Crash on Concurrency Fix#121
Bryan Malumphy (bmalumphy) merged 4 commits intomainfrom
bugfix/chat-crashes-on-observation

Conversation

@bmalumphy
Copy link
Copy Markdown
Contributor

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_checkIsolatedSwift checks. Several patterns that compiled and ran
correctly under Swift 5.x now crash with EXC_BREAKPOINT in _dispatch_assert_queue_fail
when those checks fire on the wrong thread. This PR fixes all four affected sites.


Root cause

Swift 6.3 injects an implicit dispatch_assert_queue check:

  • At the prologue of any closure created inside an @MainActor method, if
    that closure escapes to a non-@Sendable context (e.g. a Ditto callback).
  • At call sites where @MainActor-isolated methods are invoked from
    nonisolated Combine sinks, even when the sink is connected with
    .receive(on: DispatchQueue.main) — GCD queues and the Swift concurrency
    main actor executor are distinct; Ditto also ignores deliverOn: .main and
    delivers on utility-qos regardless.

Changes

ChatNotificationManager.swift

Problem: startObserving(room:) is @MainActor. Swift 6.3 injected an
isolation check into the prologue of the registerObserver closure defined
inside it. When Ditto delivered the callback on utility-qos, that check
crashed immediately — before any user code in the closure ran (frame shown as
ChatNotificationManager.swift:0 in the backtrace).

Fix:

  • Changed import DittoSwift@preconcurrency import DittoSwift to suppress
    compile-time sendability errors on the non-@Sendable callback type.
  • Extracted registerObserver into a new nonisolated method makeObserver(...).
    Closures defined in a nonisolated context carry no @MainActor coloring, so
    no prologue check is injected.
  • Inside makeObserver, used nonisolated(unsafe) weak var weakSelf to safely
    reference self without actor-region taint, then hopped back to main via
    DispatchQueue.main.async { MainActor.assumeIsolated { } }.

DittoChat.swift

Problem: The Combine sink on p2pStore.publicRoomsPublisher called
manager?.syncRooms(rooms) — an @MainActor method — from a nonisolated sink
using .receive(on: DispatchQueue.main). In Swift 6.3, this triggers a runtime
isolation check because the Swift concurrency main actor executor and
DispatchQueue.main are distinct.

Fix: Removed .receive(on: DispatchQueue.main). Wrapped the syncRooms
call in Task { @MainActor [weak manager] in } so it enters @MainActor
context through the Swift concurrency executor.

Publishers.swiftDittoStore.observePublisher

Problem: Both observePublisher overloads sent values from the Ditto
callback thread directly to downstream subscribers (e.g. assign(to: \.allPublicRooms)
in DittoService, @Published properties in ViewModels). Despite deliverOn: .main
being passed to registerObserver, Ditto delivers on utility-qos. Swift 6.3
checks isolation at each @MainActor property write, crashing when those writes
arrived off-main.

Fix: Added .receive(on: DispatchQueue.main) before eraseToAnyPublisher()
in both overloads so all downstream subscribers are guaranteed to receive on main.

Concurrency.swiftasyncMap

Problem: value (of type Output) was captured by the Future closure
and separately needed to be sending to the Task closure. Swift 6.3 region
isolation analysis flagged value as potentially aliased between the two
closures. Additionally, Future.Promise is not Sendable, causing a sending
parameter error when captured by Task.

Fix:

  • Removed @MainActor from both asyncMap overloads; added Output: Sendable
    constraint on the extension and T: Sendable + @Sendable on the transform.
  • Introduced SendableBox<T>: @unchecked Sendable to wrap both value (vBox)
    and promise (pBox) inside each Future closure, breaking the region-
    isolation alias analysis and satisfying the sending closure requirement.

Threading model after this PR

Site Before After
ChatNotificationManager observer callback @MainActor-colored closure, no hop → crash nonisolated closure → DispatchQueue.main.asyncMainActor.assumeIsolated
DittoChat rooms sink .receive(on: DispatchQueue.main) + nonisolated call → crash Task { @MainActor in } via Swift concurrency executor
observePublisher downstream Raw Ditto callback thread → crash on @MainActor writes .receive(on: DispatchQueue.main) before subscribers
asyncMap Task captures Region-isolation violations → compile error SendableBox wrappers bypass alias analysis

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 30, 2026

🟢 Test Coverage Report - @dittolive/ditto-chat-core

Overall Coverage: 87.93%

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%)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Mar 30, 2026

🟢 Test Coverage Report - @dittolive/ditto-chat-ui

Overall Coverage: 89.67%

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%)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can not wait for v5 so we can drop all this stuff we have to do to work around it

@bmalumphy Bryan Malumphy (bmalumphy) merged commit 1a23569 into main Apr 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants