crossmate

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

commit 70aa522b5e93119b2f42f89931a10e68996f4911
parent e8d062b7a3742d6a87f4ee64b64322026ea9496e
Author: Michael Camilleri <[email protected]>
Date:   Thu,  2 Jul 2026 02:59:52 +0900

Stop blocking sync actors on Core Data queues

This commit converts the remaining audited sync-actor Core Data writes
from performAndWait to await context.perform. These paths were not
blocking the main actor, but they still held a cooperative executor
thread while the private Core Data queue did its work, which is a
runtime scaling risk during sync bursts and debounced cursor or move
flushes.

SyncEngine fetched-record application, MovesUpdater persistence, and
PlayerSelectionPublisher cursor writes now suspend their actor while the
context queue runs. MovesUpdater also snapshots its event log before
entering the escaping Core Data closure, keeping the save-failure log
path available without capturing actor-isolated state.

Co-Authored-By: Codex GPT 5.5 <[email protected]>

Diffstat:
MCrossmate/Sync/MovesUpdater.swift | 8++++----
MCrossmate/Sync/PlayerSelectionPublisher.swift | 2+-
MCrossmate/Sync/SyncEngine.swift | 2+-
3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/Crossmate/Sync/MovesUpdater.swift b/Crossmate/Sync/MovesUpdater.swift @@ -126,7 +126,7 @@ actor MovesUpdater { let snapshot = buffer buffer.removeAll(keepingCapacity: true) - guard let affected = persistAndMerge( + guard let affected = await persistAndMerge( snapshot: snapshot, writerAuthorID: writerAuthorID ) else { @@ -145,7 +145,7 @@ actor MovesUpdater { private func persistAndMerge( snapshot: [Key: Pending], writerAuthorID: String - ) -> Set<UUID>? { + ) async -> Set<UUID>? { let context = persistence.container.newBackgroundContext() // This flush links MovesEntity/CellEntity to their GameEntity and bumps // game.updatedAt, while the sync engine writes the same rows from its own @@ -155,7 +155,8 @@ actor MovesUpdater { // the rest of the sync layer with mergeByPropertyObjectTrump, which also // keeps the grid fail-safe to the local typist on a same-cell race. context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump - return context.performAndWait { + let eventLog = persistence.eventLog + return await context.perform { var byGame: [UUID: [(Key, Pending)]] = [:] for (key, pending) in snapshot { byGame[key.gameID, default: []].append((key, pending)) @@ -219,7 +220,6 @@ actor MovesUpdater { try context.save() } catch { let message = "MovesUpdater: failed to save context: \(error)" - let eventLog = persistence.eventLog Task { @MainActor in eventLog?.note(message, level: "error") } diff --git a/Crossmate/Sync/PlayerSelectionPublisher.swift b/Crossmate/Sync/PlayerSelectionPublisher.swift @@ -200,7 +200,7 @@ actor PlayerSelectionPublisher { context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let now = Date() let fallbackName = self.fallbackName - context.performAndWait { + await context.perform { let recordName = RecordSerializer.recordName(forPlayerInGame: gameID, authorID: authorID) let req = NSFetchRequest<PlayerEntity>(entityName: "PlayerEntity") req.predicate = NSPredicate(format: "ckRecordName == %@", recordName) diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift @@ -1463,7 +1463,7 @@ actor SyncEngine { let ctx = persistence.container.newBackgroundContext() ctx.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump let localAuthorID = await currentLocalAuthorID() - let effects: BatchEffects = ctx.performAndWait { + let effects: BatchEffects = await ctx.perform { var effects = BatchEffects() for mod in event.modifications { let record = mod.record