commit 386810a1adddbbd83b4831dc91c82d215b232ac4
parent fdf1e2aec6ca54b274312346c5861d21cb8941b2
Author: Michael Camilleri <[email protected]>
Date: Wed, 27 May 2026 15:37:50 +0900
Use private struct rather than long tuple for return value
Diffstat:
2 files changed, 66 insertions(+), 67 deletions(-)
diff --git a/Crossmate/Sync/RecordApplier.swift b/Crossmate/Sync/RecordApplier.swift
@@ -16,6 +16,17 @@ struct AuthorDelta: Sendable {
let latestUpdate: Date
}
+struct BatchEffects {
+ var movesUpdated = Set<UUID>()
+ var affected = Set<UUID>()
+ var pings: [Ping] = []
+ var playersUpdated = Set<UUID>()
+ var playerPresenceChanged = Set<UUID>()
+ var removed = Set<UUID>()
+ var readCursors: [(UUID, Date)] = []
+ var authorDeltas: [AuthorDelta] = []
+}
+
extension SyncEngine {
func applyDirectRecordZoneChanges(
records: [CKRecord],
@@ -26,13 +37,8 @@ extension SyncEngine {
let ctx = persistence.container.newBackgroundContext()
ctx.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
let localAuthorID = await currentLocalAuthorID()
- let (movesUpdatedGameIDs, affectedGameIDs, playersUpdatedGameIDs, playerPresenceChangedGameIDs, readCursors, authorDeltas):
- (Set<UUID>, Set<UUID>, Set<UUID>, Set<UUID>, [(UUID, Date)], [AuthorDelta]) = ctx.performAndWait {
- var movesUpdated = Set<UUID>()
- var affected = Set<UUID>()
- var playersUpdated = Set<UUID>()
- var playerPresenceChanged = Set<UUID>()
- var read: [(UUID, Date)] = []
+ let effects: BatchEffects = ctx.performAndWait {
+ var effects = BatchEffects()
// Pre-pass: snapshot the merged grid for every game that has a
// Moves record in this batch, so the post-apply diff can attribute
// each cell transition to the LWW-winning writer.
@@ -47,7 +53,7 @@ extension SyncEngine {
switch record.recordType {
case "Game":
let entity = RecordSerializer.applyGameRecord(record, to: ctx, databaseScope: scopeValue)
- if let id = entity.id { affected.insert(id) }
+ if let id = entity.id { effects.affected.insert(id) }
case "Moves":
if let value = RecordSerializer.parseMovesRecord(record) {
let cellsChanged = RecordSerializer.applyMovesRecord(
@@ -56,8 +62,8 @@ extension SyncEngine {
to: ctx,
localAuthorID: localAuthorID
)
- if cellsChanged { movesUpdated.insert(value.gameID) }
- affected.insert(value.gameID)
+ if cellsChanged { effects.movesUpdated.insert(value.gameID) }
+ effects.affected.insert(value.gameID)
}
case "Player":
if let (gameID, _) = RecordSerializer.parsePlayerRecordName(record.recordID.recordName) {
@@ -65,11 +71,11 @@ extension SyncEngine {
record,
in: ctx,
localAuthorID: localAuthorID,
- onFirstTime: { playersUpdated.insert($0) },
- onPresenceChange: { playerPresenceChanged.insert($0) },
- onReadCursor: { read.append(($0, $1)) }
+ onFirstTime: { effects.playersUpdated.insert($0) },
+ onPresenceChange: { effects.playerPresenceChanged.insert($0) },
+ onReadCursor: { effects.readCursors.append(($0, $1)) }
)
- affected.insert(gameID)
+ effects.affected.insert(gameID)
}
default:
break
@@ -82,14 +88,14 @@ extension SyncEngine {
in: ctx
)
if let id = self.gameID(fromRecordName: deletion.0.recordName) {
- affected.insert(id)
+ effects.affected.insert(id)
}
}
- for gameID in movesUpdated {
+ for gameID in effects.movesUpdated {
self.replayCellCache(for: gameID, in: ctx)
}
let afterGrids = Self.gridSnapshots(for: movesBearingGameIDs, in: ctx)
- let deltas = Self.authorDeltas(
+ effects.authorDeltas = Self.authorDeltas(
before: beforeGrids,
after: afterGrids,
cutoff: Date().addingTimeInterval(-SessionMonitor.quiescenceWindow)
@@ -106,23 +112,23 @@ extension SyncEngine {
)
}
}
- return (movesUpdated, affected, playersUpdated, playerPresenceChanged, read, deltas)
+ return effects
}
- if let onRemoteMovesUpdated, !movesUpdatedGameIDs.isEmpty {
- await onRemoteMovesUpdated(movesUpdatedGameIDs)
+ if let onRemoteMovesUpdated, !effects.movesUpdated.isEmpty {
+ await onRemoteMovesUpdated(effects.movesUpdated)
}
- if let onRemoteAuthorDelta, !authorDeltas.isEmpty {
- await onRemoteAuthorDelta(authorDeltas)
+ if let onRemoteAuthorDelta, !effects.authorDeltas.isEmpty {
+ await onRemoteAuthorDelta(effects.authorDeltas)
}
- if let onRemotePlayersUpdated, !playersUpdatedGameIDs.isEmpty {
- await onRemotePlayersUpdated(playersUpdatedGameIDs)
+ if let onRemotePlayersUpdated, !effects.playersUpdated.isEmpty {
+ await onRemotePlayersUpdated(effects.playersUpdated)
}
- if let onRemotePlayerPresenceChanged, !playerPresenceChangedGameIDs.isEmpty {
- await onRemotePlayerPresenceChanged(playerPresenceChangedGameIDs)
+ if let onRemotePlayerPresenceChanged, !effects.playerPresenceChanged.isEmpty {
+ await onRemotePlayerPresenceChanged(effects.playerPresenceChanged)
}
- if let onIncomingReadCursor, !readCursors.isEmpty {
- await onIncomingReadCursor(readCursors)
+ if let onIncomingReadCursor, !effects.readCursors.isEmpty {
+ await onIncomingReadCursor(effects.readCursors)
}
let pingDeletedGameIDs = Set(deletions.compactMap { deletion -> UUID? in
deletion.0.recordName.hasPrefix("ping-")
@@ -131,11 +137,11 @@ extension SyncEngine {
if let onPingDeleted, !pingDeletedGameIDs.isEmpty {
await onPingDeleted(pingDeletedGameIDs)
}
- if !affectedGameIDs.isEmpty {
+ if !effects.affected.isEmpty {
NotificationCenter.default.post(
name: .playerRosterShouldRefresh,
object: nil,
- userInfo: ["gameIDs": affectedGameIDs]
+ userInfo: ["gameIDs": effects.affected]
)
}
}
diff --git a/Crossmate/Sync/SyncEngine.swift b/Crossmate/Sync/SyncEngine.swift
@@ -1168,15 +1168,8 @@ actor SyncEngine {
let ctx = persistence.container.newBackgroundContext()
ctx.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
let localAuthorID = await currentLocalAuthorID()
- let (movesUpdatedGameIDs, affectedGameIDs, pings, playersUpdatedGameIDs, playerPresenceChangedGameIDs, removedGameIDs, readCursors, authorDeltas):
- (Set<UUID>, Set<UUID>, [Ping], Set<UUID>, Set<UUID>, Set<UUID>, [(UUID, Date)], [AuthorDelta]) = ctx.performAndWait {
- var movesUpdated = Set<UUID>()
- var affected = Set<UUID>()
- var pings: [Ping] = []
- var playersUpdated = Set<UUID>()
- var playerPresenceChanged = Set<UUID>()
- var removed = Set<UUID>()
- var read: [(UUID, Date)] = []
+ let effects: BatchEffects = ctx.performAndWait {
+ var effects = BatchEffects()
// Pre-pass: snapshot the merged grid for every game that has a
// Moves record in this batch, so the post-apply diff can attribute
// each cell transition to the LWW-winning writer.
@@ -1192,7 +1185,7 @@ actor SyncEngine {
switch record.recordType {
case "Game":
let entity = RecordSerializer.applyGameRecord(record, to: ctx, databaseScope: scope)
- if let id = entity.id { affected.insert(id) }
+ if let id = entity.id { effects.affected.insert(id) }
case "Moves":
if let value = RecordSerializer.parseMovesRecord(record) {
let cellsChanged = RecordSerializer.applyMovesRecord(
@@ -1201,8 +1194,8 @@ actor SyncEngine {
to: ctx,
localAuthorID: localAuthorID
)
- if cellsChanged { movesUpdated.insert(value.gameID) }
- affected.insert(value.gameID)
+ if cellsChanged { effects.movesUpdated.insert(value.gameID) }
+ effects.affected.insert(value.gameID)
}
case "Player":
if let (gameID, _) = RecordSerializer.parsePlayerRecordName(record.recordID.recordName) {
@@ -1210,15 +1203,15 @@ actor SyncEngine {
record,
in: ctx,
localAuthorID: localAuthorID,
- onFirstTime: { playersUpdated.insert($0) },
- onPresenceChange: { playerPresenceChanged.insert($0) },
- onReadCursor: { read.append(($0, $1)) }
+ onFirstTime: { effects.playersUpdated.insert($0) },
+ onPresenceChange: { effects.playerPresenceChanged.insert($0) },
+ onReadCursor: { effects.readCursors.append(($0, $1)) }
)
- affected.insert(gameID)
+ effects.affected.insert(gameID)
}
case "Ping":
if let ping = Ping.parseRecord(record) {
- pings.append(ping)
+ effects.pings.append(ping)
}
case "Decision":
let wrote = RecordSerializer.applyDecisionRecord(
@@ -1235,7 +1228,7 @@ actor SyncEngine {
),
dKind == "left",
let gid = UUID(uuidString: dKey) {
- removed.insert(gid)
+ effects.removed.insert(gid)
}
default:
break
@@ -1248,14 +1241,14 @@ actor SyncEngine {
in: ctx
)
if let id = self.gameID(fromRecordName: deletion.recordID.recordName) {
- affected.insert(id)
+ effects.affected.insert(id)
}
}
- for gameID in movesUpdated {
+ for gameID in effects.movesUpdated {
self.replayCellCache(for: gameID, in: ctx)
}
let afterGrids = Self.gridSnapshots(for: movesBearingGameIDs, in: ctx)
- let deltas = Self.authorDeltas(
+ effects.authorDeltas = Self.authorDeltas(
before: beforeGrids,
after: afterGrids,
cutoff: Date().addingTimeInterval(-SessionMonitor.quiescenceWindow)
@@ -1277,28 +1270,28 @@ actor SyncEngine {
)
}
}
- return (movesUpdated, affected, pings, playersUpdated, playerPresenceChanged, removed, read, deltas)
+ return effects
}
- if let onRemoteMovesUpdated, !movesUpdatedGameIDs.isEmpty {
- await onRemoteMovesUpdated(movesUpdatedGameIDs)
+ if let onRemoteMovesUpdated, !effects.movesUpdated.isEmpty {
+ await onRemoteMovesUpdated(effects.movesUpdated)
}
- if let onRemoteAuthorDelta, !authorDeltas.isEmpty {
- await onRemoteAuthorDelta(authorDeltas)
+ if let onRemoteAuthorDelta, !effects.authorDeltas.isEmpty {
+ await onRemoteAuthorDelta(effects.authorDeltas)
}
- if let onRemotePlayersUpdated, !playersUpdatedGameIDs.isEmpty {
- await onRemotePlayersUpdated(playersUpdatedGameIDs)
+ if let onRemotePlayersUpdated, !effects.playersUpdated.isEmpty {
+ await onRemotePlayersUpdated(effects.playersUpdated)
}
- if let onRemotePlayerPresenceChanged, !playerPresenceChangedGameIDs.isEmpty {
- await onRemotePlayerPresenceChanged(playerPresenceChangedGameIDs)
+ if let onRemotePlayerPresenceChanged, !effects.playerPresenceChanged.isEmpty {
+ await onRemotePlayerPresenceChanged(effects.playerPresenceChanged)
}
- if let onIncomingReadCursor, !readCursors.isEmpty {
- await onIncomingReadCursor(readCursors)
+ if let onIncomingReadCursor, !effects.readCursors.isEmpty {
+ await onIncomingReadCursor(effects.readCursors)
}
- if let onPings, !pings.isEmpty {
- await onPings(pings)
+ if let onPings, !effects.pings.isEmpty {
+ await onPings(effects.pings)
}
- for id in removedGameIDs {
+ for id in effects.removed {
if let cb = onGameRemoved { await cb(id) }
}
let pingDeletedGameIDs = Set(event.deletions.compactMap { deletion -> UUID? in
@@ -1308,11 +1301,11 @@ actor SyncEngine {
if let onPingDeleted, !pingDeletedGameIDs.isEmpty {
await onPingDeleted(pingDeletedGameIDs)
}
- if !affectedGameIDs.isEmpty {
+ if !effects.affected.isEmpty {
NotificationCenter.default.post(
name: .playerRosterShouldRefresh,
object: nil,
- userInfo: ["gameIDs": affectedGameIDs]
+ userInfo: ["gameIDs": effects.affected]
)
}
}