Fixed major issue with sync logic. Should be stable now. Solidified with

tests... turns out syncing is hard...
This commit is contained in:
2025-10-06 18:04:56 -06:00
parent a19ff8ef66
commit 603a683ab2
4 changed files with 172 additions and 49 deletions

View File

@@ -595,66 +595,94 @@ class SyncService(private val context: Context, private val repository: ClimbRep
repository.resetAllData()
// Filter out deleted gyms before importing
val deletedGymIds = backup.deletedItems.filter { it.type == "gym" }.map { it.id }.toSet()
backup.gyms.forEach { backupGym ->
try {
val gym = backupGym.toGym()
Log.d(TAG, "Importing gym: ${gym.name} (ID: ${gym.id})")
repository.insertGymWithoutSync(gym)
if (!deletedGymIds.contains(backupGym.id)) {
val gym = backupGym.toGym()
Log.d(TAG, "Importing gym: ${gym.name} (ID: ${gym.id})")
repository.insertGymWithoutSync(gym)
} else {
Log.d(TAG, "Skipping import of deleted gym: ${backupGym.id}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import gym '${backupGym.name}': ${e.message}")
throw e
}
}
// Filter out deleted problems before importing
val deletedProblemIds =
backup.deletedItems.filter { it.type == "problem" }.map { it.id }.toSet()
backup.problems.forEach { backupProblem ->
try {
val updatedProblem =
if (imagePathMapping.isNotEmpty()) {
val newImagePaths =
backupProblem.imagePaths?.map { oldPath ->
val filename = oldPath.substringAfterLast('/')
if (!deletedProblemIds.contains(backupProblem.id)) {
val updatedProblem =
if (imagePathMapping.isNotEmpty()) {
val newImagePaths =
backupProblem.imagePaths?.map { oldPath ->
val filename = oldPath.substringAfterLast('/')
imagePathMapping[filename]
?: if (ImageNamingUtils.isValidImageFilename(
filename
)
) {
"problem_images/$filename"
} else {
val index =
backupProblem.imagePaths.indexOf(
oldPath
imagePathMapping[filename]
?: if (ImageNamingUtils.isValidImageFilename(
filename
)
val consistentFilename =
ImageNamingUtils.generateImageFilename(
backupProblem.id,
index
)
"problem_images/$consistentFilename"
}
}
?: emptyList()
backupProblem.withUpdatedImagePaths(newImagePaths)
} else {
backupProblem
}
repository.insertProblemWithoutSync(updatedProblem.toProblem())
) {
"problem_images/$filename"
} else {
val index =
backupProblem.imagePaths.indexOf(
oldPath
)
val consistentFilename =
ImageNamingUtils
.generateImageFilename(
backupProblem.id,
index
)
"problem_images/$consistentFilename"
}
}
?: emptyList()
backupProblem.withUpdatedImagePaths(newImagePaths)
} else {
backupProblem
}
repository.insertProblemWithoutSync(updatedProblem.toProblem())
} else {
Log.d(TAG, "Skipping import of deleted problem: ${backupProblem.id}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import problem '${backupProblem.name}': ${e.message}")
}
}
// Filter out deleted sessions before importing
val deletedSessionIds =
backup.deletedItems.filter { it.type == "session" }.map { it.id }.toSet()
backup.sessions.forEach { backupSession ->
try {
repository.insertSessionWithoutSync(backupSession.toClimbSession())
if (!deletedSessionIds.contains(backupSession.id)) {
repository.insertSessionWithoutSync(backupSession.toClimbSession())
} else {
Log.d(TAG, "Skipping import of deleted session: ${backupSession.id}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import session '${backupSession.id}': ${e.message}")
}
}
// Filter out deleted attempts before importing
val deletedAttemptIds =
backup.deletedItems.filter { it.type == "attempt" }.map { it.id }.toSet()
backup.attempts.forEach { backupAttempt ->
try {
repository.insertAttemptWithoutSync(backupAttempt.toAttempt())
if (!deletedAttemptIds.contains(backupAttempt.id)) {
repository.insertAttemptWithoutSync(backupAttempt.toAttempt())
} else {
Log.d(TAG, "Skipping import of deleted attempt: ${backupAttempt.id}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to import attempt '${backupAttempt.id}': ${e.message}")
}
@@ -678,6 +706,19 @@ class SyncService(private val context: Context, private val repository: ClimbRep
}
}
// Import deletion records to prevent future resurrections
backup.deletedItems.forEach { deletion ->
try {
val deletionJson = json.encodeToString(deletion)
val preferences =
context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE)
preferences.edit { putString("deleted_${deletion.id}", deletionJson) }
Log.d(TAG, "Imported deletion record: ${deletion.type} ${deletion.id}")
} catch (e: Exception) {
Log.e(TAG, "Failed to import deletion record: ${e.message}")
}
}
dataStateManager.setLastModified(backup.exportedAt)
Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}")
}
@@ -694,10 +735,27 @@ class SyncService(private val context: Context, private val repository: ClimbRep
val localSessions = repository.getAllSessions().first()
val localAttempts = repository.getAllAttempts().first()
// Store active sessions before clearing
val activeSessions = localSessions.filter { it.status == SessionStatus.ACTIVE }
// Store active sessions before clearing (but exclude any that were deleted)
val localDeletedItems = repository.getDeletedItems()
val allDeletedSessionIds =
(serverBackup.deletedItems + localDeletedItems)
.filter { it.type == "session" }
.map { it.id }
.toSet()
val activeSessions =
localSessions.filter {
it.status == SessionStatus.ACTIVE && !allDeletedSessionIds.contains(it.id)
}
val activeSessionIds = activeSessions.map { it.id }.toSet()
val activeAttempts = localAttempts.filter { activeSessionIds.contains(it.sessionId) }
val allDeletedAttemptIds =
(serverBackup.deletedItems + localDeletedItems)
.filter { it.type == "attempt" }
.map { it.id }
.toSet()
val activeAttempts =
localAttempts.filter {
activeSessionIds.contains(it.sessionId) && !allDeletedAttemptIds.contains(it.id)
}
Log.d(TAG, "Merging data...")
val mergedGyms = mergeGyms(localGyms, serverBackup.gyms, serverBackup.deletedItems)
@@ -772,9 +830,15 @@ class SyncService(private val context: Context, private val repository: ClimbRep
// Clear and update local deletions with merged list
repository.clearDeletedItems()
allDeletions.forEach { deletion ->
// Re-add merged deletions to local store
// Note: This is a simplified approach - in production you might want a more
// sophisticated merge
try {
val deletionJson = json.encodeToString(deletion)
val preferences =
context.getSharedPreferences("deleted_items", Context.MODE_PRIVATE)
preferences.edit { putString("deleted_${deletion.id}", deletionJson) }
Log.d(TAG, "Merged deletion record: ${deletion.type} ${deletion.id}")
} catch (e: Exception) {
Log.e(TAG, "Failed to save merged deletion: ${e.message}")
}
}
// Upload merged data back to server