crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 70268eec5c5389ed8af7cb346c39f30604de3e08
parent 7e228e4a0c81ad512e1707a0c1a4e53efb51db4d
Author: Michael Camilleri <[email protected]>
Date:   Sun, 14 Jun 2026 14:12:04 +0900

Fix main-actor isolation warnings in Core Data background closures

Three call sites reached main-actor-isolated state from inside a
background Core Data context closure (perform/performAndWait), which the
compiler flags as a potential data race under strict concurrency:

- InviteCoordinator.consumeStaleInvites read identity.currentID inside
  the performAndWait closure and called the main-actor-isolated
  staleInviteRecordNames. This commit hoists currentID into a local
  read on the main actor before the closure, and marks
  staleInviteRecordNames nonisolated since it only touches the context
  and its value-type parameters.

- GameStore.cacheRemoteJournals logged a save failure via the main-actor
  EventLog from inside ctx.perform. This commit builds the message on
  the background queue, then hops to the main actor to call note().

Co-Authored-By: Claude Opus 4.8 <[email protected]>

Diffstat:
MCrossmate/Persistence/GameStore.swift | 5++++-
MCrossmate/Services/InviteCoordinator.swift | 5+++--
2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/Crossmate/Persistence/GameStore.swift b/Crossmate/Persistence/GameStore.swift @@ -891,7 +891,10 @@ final class GameStore { do { try ctx.save() } catch { - self.eventLog?.note("GameStore: replay cache save failed — \(error)", level: "error") + let message = "GameStore: replay cache save failed — \(error)" + Task { @MainActor [weak self] in + self?.eventLog?.note(message, level: "error") + } } } } diff --git a/Crossmate/Services/InviteCoordinator.swift b/Crossmate/Services/InviteCoordinator.swift @@ -440,12 +440,13 @@ final class InviteCoordinator { } guard !candidates.isEmpty else { return pings } + let currentAuthorID = identity.currentID let ctx = persistence.container.newBackgroundContext() let staleNames: Set<String> = ctx.performAndWait { Self.staleInviteRecordNames( among: candidates, in: ctx, - currentAuthorID: identity.currentID + currentAuthorID: currentAuthorID ) } guard !staleNames.isEmpty else { return pings } @@ -462,7 +463,7 @@ final class InviteCoordinator { return pings.filter { !staleNames.contains($0.recordName) } } - static func staleInviteRecordNames( + nonisolated static func staleInviteRecordNames( among pings: [Ping], in ctx: NSManagedObjectContext, currentAuthorID: String?