Conflict Resolution
Conflict resolution is a critical aspect of mobile data synchronization when multiple clients can modify the same data simultaneously. This section covers various conflict detection mechanisms, resolution strategies, and implementation patterns to ensure data consistency while maintaining user experience.
Conflict Detection
Version-Based Detection
Version-based conflict detection uses version numbers or timestamps to identify when the same piece of data has been modified by multiple clients.
Optimistic Locking with Version Numbers
// iOS version-based conflict detection
struct VersionedItem {
let id: String
let title: String
let content: String
let version: Int
let lastModified: Date
let modifiedBy: String
}
class VersionConflictDetector {
func detectConflict(local: VersionedItem, server: VersionedItem) -> ConflictType? {
if local.id != server.id {
return nil // Different items
}
if local.version == server.version {
return nil // No conflict
}
if local.version < server.version {
return .serverNewer
}
if local.version > server.version {
return .clientNewer
}
// This shouldn't happen with proper versioning
return .versionMismatch
}
func hasConflict(localChanges: [VersionedItem], serverChanges: [VersionedItem]) -> [ConflictPair] {
var conflicts: [ConflictPair] = []
let serverMap = Dictionary(uniqueKeysWithValues: serverChanges.map { ($0.id, $0) })
for localItem in localChanges {
if let serverItem = serverMap[localItem.id] {
if let conflictType = detectConflict(local: localItem, server: serverItem) {
conflicts.append(ConflictPair(
local: localItem,
server: serverItem,
type: conflictType
))
}
}
}
return conflicts
}
}
enum ConflictType {
case serverNewer
case clientNewer
case versionMismatch
case simultaneousModification
}
struct ConflictPair {
let local: VersionedItem
let server: VersionedItem
let type: ConflictType
}
Timestamp-Based Detection
// Android timestamp-based conflict detection
data class TimestampedItem(
val id: String,
val title: String,
val content: String,
val lastModified: Long,
val lastSynced: Long,
val modifiedBy: String
)
class TimestampConflictDetector {
private val conflictWindowMs = 1000L // 1 second tolerance
fun detectConflict(local: TimestampedItem, server: TimestampedItem): ConflictInfo? {
if (local.id != server.id) return null
val localModifiedSinceSync = local.lastModified > local.lastSynced
val serverModifiedSinceSync = server.lastModified > local.lastSynced
if (!localModifiedSinceSync && !serverModifiedSinceSync) {
return null // No changes since last sync
}
if (localModifiedSinceSync && !serverModifiedSinceSync) {
return null // Only local changes
}
if (!localModifiedSinceSync && serverModifiedSinceSync) {
return null // Only server changes
}
// Both modified since last sync - conflict
return ConflictInfo(
localItem = local,
serverItem = server,
type = when {
Math.abs(local.lastModified - server.lastModified) < conflictWindowMs ->
ConflictType.SIMULTANEOUS
local.lastModified > server.lastModified ->
ConflictType.LOCAL_NEWER
else ->
ConflictType.SERVER_NEWER
}
)
}
fun analyzeConflicts(localChanges: List<TimestampedItem>, serverChanges: List<TimestampedItem>): List<ConflictInfo> {
val serverMap = serverChanges.associateBy { it.id }
val conflicts = mutableListOf<ConflictInfo>()
for (localItem in localChanges) {
serverMap[localItem.id]?.let { serverItem ->
detectConflict(localItem, serverItem)?.let { conflict ->
conflicts.add(conflict)
}
}
}
return conflicts
}
}
data class ConflictInfo(
val localItem: TimestampedItem,
val serverItem: TimestampedItem,
val type: ConflictType
)
enum class ConflictType {
SIMULTANEOUS,
LOCAL_NEWER,
SERVER_NEWER
}
Content-Based Detection
Content-based detection uses checksums or content hashes to identify conflicts more precisely.
// React Native content-based conflict detection
import crypto from 'crypto';
interface ContentItem {
id: string;
title: string;
content: string;
checksum: string;
lastModified: number;
}
class ContentConflictDetector {
private generateChecksum(item: Omit<ContentItem, 'checksum'>): string {
const content = JSON.stringify({
title: item.title,
content: item.content,
});
return crypto.createHash('sha256').update(content).digest('hex');
}
private validateChecksum(item: ContentItem): boolean {
const expectedChecksum = this.generateChecksum(item);
return expectedChecksum === item.checksum;
}
detectContentConflict(
local: ContentItem,
server: ContentItem,
baseVersion?: ContentItem
): ContentConflict | null {
if (local.id !== server.id) return null;
// Validate checksums
if (!this.validateChecksum(local) || !this.validateChecksum(server)) {
throw new Error('Invalid checksum detected');
}
// No conflict if content is identical
if (local.checksum === server.checksum) {
return null;
}
// Analyze changes if base version is available
if (baseVersion) {
const localChanged = local.checksum !== baseVersion.checksum;
const serverChanged = server.checksum !== baseVersion.checksum;
if (localChanged && serverChanged) {
return {
type: 'CONTENT_CONFLICT',
local,
server,
base: baseVersion,
changes: this.analyzeChanges(baseVersion, local, server),
};
}
}
// Simple conflict without base version
return {
type: 'CONTENT_CONFLICT',
local,
server,
base: baseVersion,
changes: null,
};
}
private analyzeChanges(
base: ContentItem,
local: ContentItem,
server: ContentItem
): ChangeAnalysis {
return {
titleChanged: {
local: base.title !== local.title,
server: base.title !== server.title,
},
contentChanged: {
local: base.content !== local.content,
server: base.content !== server.content,
},
localChanges: this.getFieldChanges(base, local),
serverChanges: this.getFieldChanges(base, server),
};
}
private getFieldChanges(base: ContentItem, modified: ContentItem): FieldChanges {
return {
title: base.title !== modified.title ? {
from: base.title,
to: modified.title,
} : null,
content: base.content !== modified.content ? {
from: base.content,
to: modified.content,
} : null,
};
}
}
interface ContentConflict {
type: 'CONTENT_CONFLICT';
local: ContentItem;
server: ContentItem;
base?: ContentItem;
changes: ChangeAnalysis | null;
}
interface ChangeAnalysis {
titleChanged: {
local: boolean;
server: boolean;
};
contentChanged: {
local: boolean;
server: boolean;
};
localChanges: FieldChanges;
serverChanges: FieldChanges;
}
interface FieldChanges {
title: {
from: string;
to: string;
} | null;
content: {
from: string;
to: string;
} | null;
}
Resolution Strategies
Last Writer Wins (LWW)
The simplest strategy where the most recently modified version wins.
// Flutter Last Writer Wins implementation
class LastWriterWinsResolver {
static const String strategyName = 'last_writer_wins';
Future<ResolvedItem> resolve(ConflictContext context) async {
final local = context.localItem;
final server = context.serverItem;
// Compare timestamps
if (local.lastModified > server.lastModified) {
return ResolvedItem(
item: local,
resolution: ResolutionResult.localWins,
strategy: strategyName,
metadata: {
'reason': 'Local item is newer',
'local_timestamp': local.lastModified,
'server_timestamp': server.lastModified,
},
);
} else {
return ResolvedItem(
item: server,
resolution: ResolutionResult.serverWins,
strategy: strategyName,
metadata: {
'reason': 'Server item is newer',
'local_timestamp': local.lastModified,
'server_timestamp': server.lastModified,
},
);
}
}
}
class ConflictContext {
final Item localItem;
final Item serverItem;
final Item? baseItem;
final ConflictType conflictType;
final Map<String, dynamic> additionalContext;
ConflictContext({
required this.localItem,
required this.serverItem,
this.baseItem,
required this.conflictType,
this.additionalContext = const {},
});
}
class ResolvedItem {
final Item item;
final ResolutionResult resolution;
final String strategy;
final Map<String, dynamic> metadata;
ResolvedItem({
required this.item,
required this.resolution,
required this.strategy,
required this.metadata,
});
}
enum ResolutionResult {
localWins,
serverWins,
merged,
userDecision,
}
Client Wins
Always prefer the client version, useful for user-centric applications.
// iOS Client Wins resolver
class ClientWinsResolver: ConflictResolver {
let strategyName = "client_wins"
func resolve(context: ConflictContext) async -> ResolvedItem {
let local = context.localItem
let server = context.serverItem
// Always choose local version but update version number
let resolvedItem = local.copy(
version: max(local.version, server.version) + 1,
lastModified: Date(),
resolvedAt: Date()
)
return ResolvedItem(
item: resolvedItem,
resolution: .localWins,
strategy: strategyName,
metadata: [
"reason": "Client version always preferred",
"original_local_version": local.version,
"original_server_version": server.version,
"new_version": resolvedItem.version
]
)
}
}
Server Wins
Always prefer the server version, useful for authoritative data sources.
// Android Server Wins resolver
class ServerWinsResolver : ConflictResolver {
override val strategyName = "server_wins"
override suspend fun resolve(context: ConflictContext): ResolvedItem {
val server = context.serverItem
val local = context.localItem
// Use server version but preserve local metadata if needed
val resolvedItem = server.copy(
localId = local.localId, // Preserve local ID
lastSynced = System.currentTimeMillis(),
syncStatus = SyncStatus.RESOLVED
)
return ResolvedItem(
item = resolvedItem,
resolution = ResolutionResult.SERVER_WINS,
strategy = strategyName,
metadata = mapOf(
"reason" to "Server version is authoritative",
"discarded_local_changes" to local.getChangedFields(),
"server_version" to server.version
)
)
}
}
Merge Strategy
Intelligently merge changes from both versions.
// React Native merge strategy
class MergeResolver {
strategyName = 'merge';
async resolve(context) {
const { localItem, serverItem, baseItem } = context;
if (!baseItem) {
// Without base version, use field-level merge
return this.performFieldLevelMerge(localItem, serverItem);
}
// Three-way merge with base version
return this.performThreeWayMerge(baseItem, localItem, serverItem);
}
async performFieldLevelMerge(local, server) {
const merged = {
id: local.id,
version: Math.max(local.version || 0, server.version || 0) + 1,
lastModified: Date.now(),
};
// Merge strategy for different field types
const mergeStrategies = {
title: this.mergeTextField,
content: this.mergeTextField,
tags: this.mergeArrayField,
metadata: this.mergeObjectField,
priority: this.mergeNumericField,
};
const conflicts = [];
for (const [field, strategy] of Object.entries(mergeStrategies)) {
try {
const result = strategy(local[field], server[field], field);
merged[field] = result.value;
if (result.hasConflict) {
conflicts.push({
field,
localValue: local[field],
serverValue: server[field],
resolvedValue: result.value,
resolution: result.resolution,
});
}
} catch (error) {
// Field-level merge failed, escalate to user
throw new MergeConflictError(field, local[field], server[field]);
}
}
return {
item: merged,
resolution: 'merged',
strategy: this.strategyName,
metadata: {
conflicts,
mergeType: 'field_level',
},
};
}
async performThreeWayMerge(base, local, server) {
const merged = {
id: local.id,
version: Math.max(local.version || 0, server.version || 0) + 1,
lastModified: Date.now(),
};
const conflicts = [];
for (const field of Object.keys(base)) {
const baseValue = base[field];
const localValue = local[field];
const serverValue = server[field];
const mergeResult = this.mergeThreeWayField(
baseValue,
localValue,
serverValue,
field
);
merged[field] = mergeResult.value;
if (mergeResult.hasConflict) {
conflicts.push({
field,
baseValue,
localValue,
serverValue,
resolvedValue: mergeResult.value,
resolution: mergeResult.resolution,
});
}
}
return {
item: merged,
resolution: 'merged',
strategy: this.strategyName,
metadata: {
conflicts,
mergeType: 'three_way',
},
};
}
mergeThreeWayField(base, local, server, fieldName) {
const localChanged = !this.deepEqual(base, local);
const serverChanged = !this.deepEqual(base, server);
if (!localChanged && !serverChanged) {
// No changes
return { value: base, hasConflict: false };
}
if (localChanged && !serverChanged) {
// Only local changed
return { value: local, hasConflict: false, resolution: 'local_only' };
}
if (!localChanged && serverChanged) {
// Only server changed
return { value: server, hasConflict: false, resolution: 'server_only' };
}
// Both changed
if (this.deepEqual(local, server)) {
// Same changes on both sides
return { value: local, hasConflict: false, resolution: 'identical_changes' };
}
// Actual conflict - try field-specific merge
return this.resolveFieldConflict(base, local, server, fieldName);
}
resolveFieldConflict(base, local, server, fieldName) {
switch (fieldName) {
case 'title':
case 'content':
return this.mergeTextConflict(base, local, server);
case 'tags':
return this.mergeArrayConflict(base, local, server);
case 'metadata':
return this.mergeObjectConflict(base, local, server);
default:
// Default to last writer wins for unknown fields
return {
value: server, // Prefer server
hasConflict: true,
resolution: 'server_wins_default',
};
}
}
mergeTextConflict(base, local, server) {
// Try to detect if one is an extension of the other
if (local.includes(base) && server.includes(base)) {
// Both extended the base, try to combine
const localAddition = local.replace(base, '');
const serverAddition = server.replace(base, '');
return {
value: base + localAddition + serverAddition,
hasConflict: true,
resolution: 'text_concatenated',
};
}
// Complex text conflict - escalate to user
throw new TextMergeConflictError(base, local, server);
}
mergeArrayConflict(base, local, server) {
// Merge arrays by combining unique elements
const localAdditions = local.filter(item => !base.includes(item));
const serverAdditions = server.filter(item => !base.includes(item));
const localRemovals = base.filter(item => !local.includes(item));
const serverRemovals = base.filter(item => !server.includes(item));
// Start with base, apply non-conflicting changes
let merged = [...base];
// Remove items that both sides removed
const commonRemovals = localRemovals.filter(item => serverRemovals.includes(item));
merged = merged.filter(item => !commonRemovals.includes(item));
// Add items that either side added
const allAdditions = [...localAdditions, ...serverAdditions];
const uniqueAdditions = [...new Set(allAdditions)];
merged.push(...uniqueAdditions);
return {
value: merged,
hasConflict: localRemovals.length > 0 || serverRemovals.length > 0,
resolution: 'array_merged',
};
}
mergeObjectConflict(base, local, server) {
const merged = { ...base };
const conflicts = [];
// Get all keys from all versions
const allKeys = new Set([
...Object.keys(base),
...Object.keys(local),
...Object.keys(server),
]);
for (const key of allKeys) {
const baseValue = base[key];
const localValue = local[key];
const serverValue = server[key];
if (localValue === serverValue) {
merged[key] = localValue;
} else if (localValue === baseValue) {
merged[key] = serverValue; // Server changed
} else if (serverValue === baseValue) {
merged[key] = localValue; // Local changed
} else {
// Both changed differently
merged[key] = serverValue; // Default to server
conflicts.push(key);
}
}
return {
value: merged,
hasConflict: conflicts.length > 0,
resolution: 'object_merged',
conflictedKeys: conflicts,
};
}
mergeTextField(local, server, fieldName) {
if (local === server) {
return { value: local, hasConflict: false };
}
// Simple text merge - prefer longer text if one contains the other
if (local.includes(server)) {
return {
value: local,
hasConflict: true,
resolution: 'local_contains_server'
};
}
if (server.includes(local)) {
return {
value: server,
hasConflict: true,
resolution: 'server_contains_local'
};
}
// Can't merge automatically
throw new FieldMergeConflictError(fieldName, local, server);
}
mergeArrayField(local, server, fieldName) {
// Merge arrays by union
const merged = [...new Set([...local, ...server])];
return {
value: merged,
hasConflict: local.length !== server.length,
resolution: 'array_union',
};
}
mergeObjectField(local, server, fieldName) {
// Merge objects by combining properties
const merged = { ...local, ...server };
const localKeys = Object.keys(local);
const serverKeys = Object.keys(server);
const conflictedKeys = [];
for (const key of localKeys) {
if (serverKeys.includes(key) && local[key] !== server[key]) {
conflictedKeys.push(key);
}
}
return {
value: merged,
hasConflict: conflictedKeys.length > 0,
resolution: 'object_overlay',
conflictedKeys,
};
}
mergeNumericField(local, server, fieldName) {
// For numeric fields, could use various strategies
if (local === server) {
return { value: local, hasConflict: false };
}
// Default to maximum value
return {
value: Math.max(local, server),
hasConflict: true,
resolution: 'numeric_max',
};
}
deepEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
}
class MergeConflictError extends Error {
constructor(field, localValue, serverValue) {
super(`Cannot automatically merge field: ${field}`);
this.field = field;
this.localValue = localValue;
this.serverValue = serverValue;
}
}
class TextMergeConflictError extends MergeConflictError {
constructor(base, local, server) {
super('text', local, server);
this.base = base;
}
}
class FieldMergeConflictError extends MergeConflictError {
constructor(field, local, server) {
super(field, local, server);
}
}
User-Mediated Resolution
When automatic resolution isn't possible, present conflicts to the user.
// iOS user-mediated conflict resolution
class UserMediatedResolver {
private let presentationQueue = DispatchQueue.main
func resolve(context: ConflictContext) async -> ResolvedItem {
return await withCheckedContinuation { continuation in
presentationQueue.async {
self.presentConflictResolutionUI(
context: context,
completion: { resolvedItem in
continuation.resume(returning: resolvedItem)
}
)
}
}
}
private func presentConflictResolutionUI(
context: ConflictContext,
completion: @escaping (ResolvedItem) -> Void
) {
let alertController = UIAlertController(
title: "Data Conflict Detected",
message: "The same item was modified both locally and on the server. How would you like to resolve this?",
preferredStyle: .actionSheet
)
// Option 1: Keep local version
alertController.addAction(UIAlertAction(
title: "Keep My Changes",
style: .default
) { _ in
let resolved = ResolvedItem(
item: context.localItem.copy(
version: max(context.localItem.version, context.serverItem.version) + 1,
lastModified: Date()
),
resolution: .localWins,
strategy: "user_mediated",
metadata: ["user_choice": "keep_local"]
)
completion(resolved)
})
// Option 2: Keep server version
alertController.addAction(UIAlertAction(
title: "Keep Server Changes",
style: .default
) { _ in
let resolved = ResolvedItem(
item: context.serverItem,
resolution: .serverWins,
strategy: "user_mediated",
metadata: ["user_choice": "keep_server"]
)
completion(resolved)
})
// Option 3: Show detailed comparison
alertController.addAction(UIAlertAction(
title: "Compare & Merge",
style: .default
) { _ in
self.presentDetailedComparison(context: context, completion: completion)
})
// Option 4: Postpone decision
alertController.addAction(UIAlertAction(
title: "Decide Later",
style: .cancel
) { _ in
let resolved = ResolvedItem(
item: context.localItem.copy(syncStatus: .conflicted),
resolution: .userDecision,
strategy: "user_mediated",
metadata: ["user_choice": "postpone"]
)
completion(resolved)
})
// Present the alert
if let topViewController = UIApplication.shared.topViewController {
topViewController.present(alertController, animated: true)
}
}
private func presentDetailedComparison(
context: ConflictContext,
completion: @escaping (ResolvedItem) -> Void
) {
let comparisonVC = ConflictComparisonViewController(
localItem: context.localItem,
serverItem: context.serverItem,
onResolution: completion
)
let navigationController = UINavigationController(rootViewController: comparisonVC)
if let topViewController = UIApplication.shared.topViewController {
topViewController.present(navigationController, animated: true)
}
}
}
class ConflictComparisonViewController: UIViewController {
private let localItem: Item
private let serverItem: Item
private let onResolution: (ResolvedItem) -> Void
private var resolvedItem: Item
init(localItem: Item, serverItem: Item, onResolution: @escaping (ResolvedItem) -> Void) {
self.localItem = localItem
self.serverItem = serverItem
self.onResolution = onResolution
self.resolvedItem = localItem // Default to local
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
title = "Resolve Conflict"
view.backgroundColor = .systemBackground
// Navigation buttons
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .cancel,
target: self,
action: #selector(cancelTapped)
)
navigationItem.rightBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .done,
target: self,
action: #selector(doneTapped)
)
setupComparisonView()
}
private func setupComparisonView() {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
let contentView = UIStackView()
contentView.axis = .vertical
contentView.spacing = 20
contentView.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(contentView)
// Title comparison
contentView.addArrangedSubview(createFieldComparison(
fieldName: "Title",
localValue: localItem.title,
serverValue: serverItem.title,
currentValue: resolvedItem.title,
onLocalSelected: { [weak self] in
self?.resolvedItem.title = self?.localItem.title ?? ""
self?.updateFieldComparison()
},
onServerSelected: { [weak self] in
self?.resolvedItem.title = self?.serverItem.title ?? ""
self?.updateFieldComparison()
}
))
// Content comparison
contentView.addArrangedSubview(createFieldComparison(
fieldName: "Content",
localValue: localItem.content,
serverValue: serverItem.content,
currentValue: resolvedItem.content,
onLocalSelected: { [weak self] in
self?.resolvedItem.content = self?.localItem.content ?? ""
self?.updateFieldComparison()
},
onServerSelected: { [weak self] in
self?.resolvedItem.content = self?.serverItem.content ?? ""
self?.updateFieldComparison()
}
))
// Constraints
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
contentView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20),
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20),
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -20),
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40)
])
}
@objc private func cancelTapped() {
let resolved = ResolvedItem(
item: localItem.copy(syncStatus: .conflicted),
resolution: .userDecision,
strategy: "user_mediated",
metadata: ["user_choice": "cancelled"]
)
dismiss(animated: true) {
self.onResolution(resolved)
}
}
@objc private func doneTapped() {
let resolved = ResolvedItem(
item: resolvedItem.copy(
version: max(localItem.version, serverItem.version) + 1,
lastModified: Date()
),
resolution: .merged,
strategy: "user_mediated",
metadata: [
"user_choice": "manual_merge",
"fields_from_local": getFieldsFromSource(resolvedItem, localItem),
"fields_from_server": getFieldsFromSource(resolvedItem, serverItem)
]
)
dismiss(animated: true) {
self.onResolution(resolved)
}
}
private func getFieldsFromSource(_ resolved: Item, _ source: Item) -> [String] {
var fields: [String] = []
if resolved.title == source.title {
fields.append("title")
}
if resolved.content == source.content {
fields.append("content")
}
return fields
}
}
Advanced Conflict Resolution
Semantic Conflict Resolution
# Python semantic conflict resolver
import difflib
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class SemanticChange:
field: str
change_type: str # 'addition', 'deletion', 'modification'
location: int
old_value: Any
new_value: Any
confidence: float
class SemanticConflictResolver:
def __init__(self):
self.field_resolvers = {
'text': self.resolve_text_semantically,
'list': self.resolve_list_semantically,
'object': self.resolve_object_semantically,
}
def resolve(self, base: Dict, local: Dict, server: Dict) -> Dict[str, Any]:
"""Resolve conflicts using semantic analysis"""
resolved = base.copy()
conflicts = []
for field in set(base.keys()) | set(local.keys()) | set(server.keys()):
base_val = base.get(field)
local_val = local.get(field)
server_val = server.get(field)
# Determine field type
field_type = self._determine_field_type(base_val, local_val, server_val)
if field_type in self.field_resolvers:
try:
resolution = self.field_resolvers[field_type](
base_val, local_val, server_val
)
resolved[field] = resolution['value']
if resolution['has_conflict']:
conflicts.append({
'field': field,
'resolution': resolution,
})
except Exception as e:
# Fallback to simple resolution
resolved[field] = self._simple_resolve(base_val, local_val, server_val)
conflicts.append({
'field': field,
'error': str(e),
'fallback_used': True,
})
else:
resolved[field] = self._simple_resolve(base_val, local_val, server_val)
return {
'resolved': resolved,
'conflicts': conflicts,
}
def resolve_text_semantically(self, base: str, local: str, server: str) -> Dict[str, Any]:
"""Resolve text conflicts using diff analysis"""
if not base:
# No base version, simple merge
if local == server:
return {'value': local, 'has_conflict': False}
else:
return {'value': f"{local}\n{server}", 'has_conflict': True, 'method': 'concatenation'}
# Analyze changes
local_changes = self._analyze_text_changes(base, local)
server_changes = self._analyze_text_changes(base, server)
# Check for overlapping changes
overlaps = self._find_change_overlaps(local_changes, server_changes)
if not overlaps:
# No overlapping changes, can merge automatically
merged = self._merge_non_overlapping_changes(base, local_changes, server_changes)
return {'value': merged, 'has_conflict': False, 'method': 'semantic_merge'}
# Has overlapping changes, need conflict resolution
merged = self._resolve_overlapping_changes(base, local_changes, server_changes, overlaps)
return {
'value': merged,
'has_conflict': True,
'method': 'semantic_merge_with_conflicts',
'overlaps': overlaps
}
def _analyze_text_changes(self, base: str, modified: str) -> List[SemanticChange]:
"""Analyze text changes using difflib"""
changes = []
base_lines = base.splitlines()
modified_lines = modified.splitlines()
differ = difflib.SequenceMatcher(None, base_lines, modified_lines)
for tag, i1, i2, j1, j2 in differ.get_opcodes():
if tag == 'equal':
continue
elif tag == 'delete':
changes.append(SemanticChange(
field='text',
change_type='deletion',
location=i1,
old_value=base_lines[i1:i2],
new_value=[],
confidence=1.0
))
elif tag == 'insert':
changes.append(SemanticChange(
field='text',
change_type='addition',
location=i1,
old_value=[],
new_value=modified_lines[j1:j2],
confidence=1.0
))
elif tag == 'replace':
changes.append(SemanticChange(
field='text',
change_type='modification',
location=i1,
old_value=base_lines[i1:i2],
new_value=modified_lines[j1:j2],
confidence=1.0
))
return changes
def _find_change_overlaps(self, local_changes: List[SemanticChange],
server_changes: List[SemanticChange]) -> List[Dict]:
"""Find overlapping changes between local and server"""
overlaps = []
for local_change in local_changes:
for server_change in server_changes:
overlap = self._calculate_overlap(local_change, server_change)
if overlap['has_overlap']:
overlaps.append({
'local_change': local_change,
'server_change': server_change,
'overlap': overlap,
})
return overlaps
def _calculate_overlap(self, change1: SemanticChange, change2: SemanticChange) -> Dict:
"""Calculate overlap between two changes"""
# Simplified overlap detection based on location
start1 = change1.location
end1 = start1 + len(change1.old_value)
start2 = change2.location
end2 = start2 + len(change2.old_value)
overlap_start = max(start1, start2)
overlap_end = min(end1, end2)
has_overlap = overlap_start < overlap_end
return {
'has_overlap': has_overlap,
'overlap_start': overlap_start if has_overlap else None,
'overlap_end': overlap_end if has_overlap else None,
'overlap_size': max(0, overlap_end - overlap_start),
}
def _merge_non_overlapping_changes(self, base: str,
local_changes: List[SemanticChange],
server_changes: List[SemanticChange]) -> str:
"""Merge non-overlapping changes"""
base_lines = base.splitlines()
result_lines = base_lines.copy()
# Sort changes by location (reverse order to maintain indices)
all_changes = [(change, 'local') for change in local_changes] + \
[(change, 'server') for change in server_changes]
all_changes.sort(key=lambda x: x[0].location, reverse=True)
for change, source in all_changes:
if change.change_type == 'deletion':
del result_lines[change.location:change.location + len(change.old_value)]
elif change.change_type == 'addition':
result_lines[change.location:change.location] = change.new_value
elif change.change_type == 'modification':
result_lines[change.location:change.location + len(change.old_value)] = change.new_value
return '\n'.join(result_lines)
def resolve_list_semantically(self, base: List, local: List, server: List) -> Dict[str, Any]:
"""Resolve list conflicts using set operations"""
if not base:
# No base, merge unique items
combined = list(set(local + server))
return {'value': combined, 'has_conflict': len(combined) != len(set(local) | set(server))}
base_set = set(base)
local_set = set(local)
server_set = set(server)
# Analyze changes
local_added = local_set - base_set
local_removed = base_set - local_set
server_added = server_set - base_set
server_removed = base_set - server_set
# Find conflicts
add_conflicts = local_added & server_removed # Local added what server removed
remove_conflicts = local_removed & server_added # Local removed what server added
# Merge without conflicts
result_set = base_set.copy()
result_set.update(local_added - remove_conflicts)
result_set.update(server_added - remove_conflicts)
result_set.difference_update(local_removed - add_conflicts)
result_set.difference_update(server_removed - add_conflicts)
has_conflict = bool(add_conflicts or remove_conflicts)
return {
'value': list(result_set),
'has_conflict': has_conflict,
'conflicts': {
'add_conflicts': list(add_conflicts),
'remove_conflicts': list(remove_conflicts),
} if has_conflict else None
}
def _determine_field_type(self, *values) -> str:
"""Determine the type of field based on values"""
for val in values:
if val is not None:
if isinstance(val, str):
return 'text'
elif isinstance(val, list):
return 'list'
elif isinstance(val, dict):
return 'object'
return 'unknown'
def _simple_resolve(self, base, local, server):
"""Simple resolution fallback"""
if local == server:
return local
elif local == base:
return server
elif server == base:
return local
else:
# Default to server wins
return server
Conflict Resolution Policies
// Android conflict resolution policy framework
interface ConflictResolutionPolicy {
fun canHandle(conflict: ConflictInfo): Boolean
suspend fun resolve(conflict: ConflictInfo): ResolutionResult
val priority: Int
}
class PolicyBasedConflictResolver(
private val policies: List<ConflictResolutionPolicy>
) {
suspend fun resolve(conflict: ConflictInfo): ResolutionResult {
val applicablePolicies = policies
.filter { it.canHandle(conflict) }
.sortedByDescending { it.priority }
for (policy in applicablePolicies) {
try {
val result = policy.resolve(conflict)
if (result.isSuccessful) {
return result
}
} catch (e: Exception) {
// Policy failed, try next one
continue
}
}
// No policy could resolve, escalate to user
return ResolutionResult.requiresUserIntervention(conflict)
}
}
// Specific policies
class TimestampPolicy : ConflictResolutionPolicy {
override val priority = 100
override fun canHandle(conflict: ConflictInfo): Boolean {
return conflict.hasTimestamps()
}
override suspend fun resolve(conflict: ConflictInfo): ResolutionResult {
val local = conflict.localItem
val server = conflict.serverItem
return if (local.lastModified > server.lastModified) {
ResolutionResult.success(local, "timestamp_local_newer")
} else {
ResolutionResult.success(server, "timestamp_server_newer")
}
}
}
class ContentSizePolicy : ConflictResolutionPolicy {
override val priority = 50
override fun canHandle(conflict: ConflictInfo): Boolean {
return conflict.isTextContent()
}
override suspend fun resolve(conflict: ConflictInfo): ResolutionResult {
val local = conflict.localItem
val server = conflict.serverItem
val localSize = local.content?.length ?: 0
val serverSize = server.content?.length ?: 0
// Prefer the version with more content
return if (localSize > serverSize) {
ResolutionResult.success(local, "content_size_local_larger")
} else {
ResolutionResult.success(server, "content_size_server_larger")
}
}
}
class UserPreferencePolicy : ConflictResolutionPolicy {
override val priority = 200
private val preferences: UserConflictPreferences
constructor(preferences: UserConflictPreferences) {
this.preferences = preferences
}
override fun canHandle(conflict: ConflictInfo): Boolean {
return preferences.hasPreferenceFor(conflict.type)
}
override suspend fun resolve(conflict: ConflictInfo): ResolutionResult {
val preference = preferences.getPreference(conflict.type)
return when (preference.strategy) {
PreferenceStrategy.ALWAYS_LOCAL ->
ResolutionResult.success(conflict.localItem, "user_preference_local")
PreferenceStrategy.ALWAYS_SERVER ->
ResolutionResult.success(conflict.serverItem, "user_preference_server")
PreferenceStrategy.ASK_USER ->
ResolutionResult.requiresUserIntervention(conflict)
PreferenceStrategy.SMART_MERGE ->
performSmartMerge(conflict)
}
}
private suspend fun performSmartMerge(conflict: ConflictInfo): ResolutionResult {
// Implement smart merge logic based on field types
return ResolutionResult.requiresUserIntervention(conflict)
}
}
class BusinessRulePolicy : ConflictResolutionPolicy {
override val priority = 150
override fun canHandle(conflict: ConflictInfo): Boolean {
return conflict.hasBusinessRules()
}
override suspend fun resolve(conflict: ConflictInfo): ResolutionResult {
// Apply business-specific rules
when (conflict.entityType) {
"order" -> return resolveOrderConflict(conflict)
"user_profile" -> return resolveUserProfileConflict(conflict)
"document" -> return resolveDocumentConflict(conflict)
}
return ResolutionResult.cannotResolve("no_business_rule")
}
private fun resolveOrderConflict(conflict: ConflictInfo): ResolutionResult {
// Orders: server version is authoritative for status changes
val local = conflict.localItem
val server = conflict.serverItem
if (server.status in listOf("cancelled", "completed", "shipped")) {
return ResolutionResult.success(server, "order_status_authoritative")
}
// For other fields, prefer local changes
return ResolutionResult.success(local, "order_local_preferred")
}
private fun resolveUserProfileConflict(conflict: ConflictInfo): ResolutionResult {
// User profiles: merge non-conflicting fields, user decides on conflicts
val merged = mergeUserProfile(conflict.localItem, conflict.serverItem)
return if (merged.hasConflicts) {
ResolutionResult.requiresUserIntervention(conflict)
} else {
ResolutionResult.success(merged.item, "user_profile_merged")
}
}
private fun resolveDocumentConflict(conflict: ConflictInfo): ResolutionResult {
// Documents: preserve collaborative edits
return ResolutionResult.requiresUserIntervention(conflict)
}
}
data class UserConflictPreferences(
private val preferences: Map<String, ConflictPreference>
) {
fun hasPreferenceFor(conflictType: String): Boolean {
return preferences.containsKey(conflictType)
}
fun getPreference(conflictType: String): ConflictPreference {
return preferences[conflictType] ?: ConflictPreference.default()
}
}
data class ConflictPreference(
val strategy: PreferenceStrategy,
val applyToSubtypes: Boolean = false,
val conditions: List<String> = emptyList()
) {
companion object {
fun default() = ConflictPreference(PreferenceStrategy.ASK_USER)
}
}
enum class PreferenceStrategy {
ALWAYS_LOCAL,
ALWAYS_SERVER,
ASK_USER,
SMART_MERGE
}
Monitoring and Analytics
Conflict Metrics Collection
// TypeScript conflict analytics
interface ConflictMetrics {
conflictId: string;
entityType: string;
entityId: string;
detectionTime: number;
resolutionTime?: number;
resolutionStrategy: string;
resolutionMethod: 'automatic' | 'user_mediated' | 'policy_based';
conflictType: string;
fieldsInConflict: string[];
resolutionDuration?: number;
userInterventionRequired: boolean;
automaticResolutionSuccess: boolean;
}
class ConflictAnalytics {
private metrics: ConflictMetrics[] = [];
private analyticsService: AnalyticsService;
constructor(analyticsService: AnalyticsService) {
this.analyticsService = analyticsService;
}
trackConflictDetected(conflict: ConflictInfo): string {
const conflictId = generateUniqueId();
const metrics: ConflictMetrics = {
conflictId,
entityType: conflict.entityType,
entityId: conflict.entityId,
detectionTime: Date.now(),
resolutionStrategy: 'unknown',
resolutionMethod: 'automatic', // Default assumption
conflictType: conflict.type,
fieldsInConflict: conflict.fieldsInConflict,
userInterventionRequired: false,
automaticResolutionSuccess: false,
};
this.metrics.push(metrics);
this.analyticsService.track('conflict_detected', {
entityType: conflict.entityType,
conflictType: conflict.type,
fieldsCount: conflict.fieldsInConflict.length,
detectionTime: metrics.detectionTime,
});
return conflictId;
}
trackConflictResolved(
conflictId: string,
resolution: ResolutionResult
): void {
const metrics = this.metrics.find(m => m.conflictId === conflictId);
if (!metrics) return;
metrics.resolutionTime = Date.now();
metrics.resolutionDuration = metrics.resolutionTime - metrics.detectionTime;
metrics.resolutionStrategy = resolution.strategy;
metrics.resolutionMethod = resolution.method;
metrics.userInterventionRequired = resolution.requiresUserIntervention;
metrics.automaticResolutionSuccess = resolution.isSuccessful && !resolution.requiresUserIntervention;
this.analyticsService.track('conflict_resolved', {
conflictId,
resolutionStrategy: resolution.strategy,
resolutionMethod: resolution.method,
resolutionDuration: metrics.resolutionDuration,
automaticResolution: metrics.automaticResolutionSuccess,
userIntervention: metrics.userInterventionRequired,
});
// Report aggregate metrics periodically
this.reportAggregateMetrics();
}
trackConflictEscalated(conflictId: string, reason: string): void {
const metrics = this.metrics.find(m => m.conflictId === conflictId);
if (!metrics) return;
metrics.userInterventionRequired = true;
metrics.resolutionMethod = 'user_mediated';
this.analyticsService.track('conflict_escalated', {
conflictId,
reason,
entityType: metrics.entityType,
conflictType: metrics.conflictType,
});
}
private reportAggregateMetrics(): void {
const last24Hours = Date.now() - (24 * 60 * 60 * 1000);
const recentMetrics = this.metrics.filter(m => m.detectionTime > last24Hours);
if (recentMetrics.length === 0) return;
const totalConflicts = recentMetrics.length;
const resolvedConflicts = recentMetrics.filter(m => m.resolutionTime).length;
const automaticResolutions = recentMetrics.filter(m => m.automaticResolutionSuccess).length;
const userInterventions = recentMetrics.filter(m => m.userInterventionRequired).length;
const avgResolutionTime = recentMetrics
.filter(m => m.resolutionDuration)
.reduce((sum, m) => sum + m.resolutionDuration!, 0) / resolvedConflicts;
// Group by entity type
const byEntityType = recentMetrics.reduce((acc, m) => {
acc[m.entityType] = (acc[m.entityType] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Group by conflict type
const byConflictType = recentMetrics.reduce((acc, m) => {
acc[m.conflictType] = (acc[m.conflictType] || 0) + 1;
return acc;
}, {} as Record<string, number>);
this.analyticsService.track('conflict_metrics_24h', {
totalConflicts,
resolvedConflicts,
resolutionRate: resolvedConflicts / totalConflicts,
automaticResolutions,
automaticResolutionRate: automaticResolutions / totalConflicts,
userInterventions,
userInterventionRate: userInterventions / totalConflicts,
avgResolutionTimeMs: avgResolutionTime,
conflictsByEntityType: byEntityType,
conflictsByType: byConflictType,
});
}
getConflictReport(timeRange: TimeRange): ConflictReport {
const filteredMetrics = this.metrics.filter(m =>
m.detectionTime >= timeRange.start && m.detectionTime <= timeRange.end
);
return {
timeRange,
totalConflicts: filteredMetrics.length,
resolvedConflicts: filteredMetrics.filter(m => m.resolutionTime).length,
pendingConflicts: filteredMetrics.filter(m => !m.resolutionTime).length,
automaticResolutions: filteredMetrics.filter(m => m.automaticResolutionSuccess).length,
userMediatedResolutions: filteredMetrics.filter(m => m.resolutionMethod === 'user_mediated').length,
averageResolutionTime: this.calculateAverageResolutionTime(filteredMetrics),
conflictDistribution: this.getConflictDistribution(filteredMetrics),
resolutionStrategies: this.getResolutionStrategies(filteredMetrics),
};
}
private calculateAverageResolutionTime(metrics: ConflictMetrics[]): number {
const resolved = metrics.filter(m => m.resolutionDuration);
if (resolved.length === 0) return 0;
return resolved.reduce((sum, m) => sum + m.resolutionDuration!, 0) / resolved.length;
}
private getConflictDistribution(metrics: ConflictMetrics[]): Record<string, number> {
return metrics.reduce((acc, m) => {
const key = `${m.entityType}:${m.conflictType}`;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
private getResolutionStrategies(metrics: ConflictMetrics[]): Record<string, number> {
return metrics
.filter(m => m.resolutionStrategy !== 'unknown')
.reduce((acc, m) => {
acc[m.resolutionStrategy] = (acc[m.resolutionStrategy] || 0) + 1;
return acc;
}, {} as Record<string, number>);
}
}
interface TimeRange {
start: number;
end: number;
}
interface ConflictReport {
timeRange: TimeRange;
totalConflicts: number;
resolvedConflicts: number;
pendingConflicts: number;
automaticResolutions: number;
userMediatedResolutions: number;
averageResolutionTime: number;
conflictDistribution: Record<string, number>;
resolutionStrategies: Record<string, number>;
}
function generateUniqueId(): string {
return `conflict_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
This comprehensive conflict resolution guide covers detection mechanisms, various resolution strategies, advanced techniques like semantic resolution, policy-based systems, and monitoring capabilities essential for robust mobile data synchronization systems.