When a user edits data offline while the server receives a concurrent update from another session, a Last-Write-Wins strategy without field-level timestamps will silently discard one of the changes. CWE-362 (Race Condition) and CWE-694 are both implicated. ISO 25010:2011 reliability.recoverability requires that the system resolves conflicts without data loss. The failure mode is particularly damaging in collaborative tools or multi-device workflows: users believe their changes were saved, but the data they see after sync reflects a different device's state. Silent data loss is harder to detect than visible errors and more damaging to user trust.
High because unresolved sync conflicts cause silent data loss or overwrites, which users discover only after the damage is done and may not be recoverable.
Implement field-level timestamps on every mutable field and a deterministic merge function. Apply the merge during sync rather than overwriting the local or remote record wholesale.
interface Record {
id: string;
title: string; titleAt: number;
description: string; descriptionAt: number;
}
function merge(local: Record, remote: Record): Record {
return {
id: local.id,
title: local.titleAt > remote.titleAt ? local.title : remote.title,
titleAt: Math.max(local.titleAt, remote.titleAt),
description: local.descriptionAt > remote.descriptionAt ? local.description : remote.description,
descriptionAt: Math.max(local.descriptionAt, remote.descriptionAt),
};
}
async function syncRecord(id: string) {
const [local, remote] = await Promise.all([getLocal(id), fetchRemote(id)]);
if (local && remote) {
const merged = merge(local, remote);
await saveLocal(merged);
await pushRemote(merged);
}
}
For fields that cannot be field-merged (e.g., binary blobs), surface a conflict prompt to the user rather than silently picking a winner.
ID: mobile-offline-storage.sync-conflicts.conflict-resolution-strategy
Severity: high
What to look for: Examine the data model and sync logic for conflict resolution. Look for Last-Write-Wins, merge strategies, user prompts, or other conflict handling approaches. Check if conflicts can even occur (e.g., field-level timestamps) and if they're handled gracefully.
Pass criteria: Count all conflict resolution code paths. Sync conflicts are handled with at least 1 defined strategy (LWW, merge, user prompt). Conflicts must not result in data loss or inconsistency. Users are informed if their changes conflicted. Before evaluating, extract and quote the conflict resolution strategy pattern found (e.g., timestamp comparison, version check, or merge function).
Fail criteria: No conflict resolution mechanism found. If simultaneous edits occur, one change could silently overwrite the other. Do NOT pass when only server-side overwrites exist without any client-side awareness of conflicts.
Skip (N/A) when: Never — conflict resolution is essential for data integrity in multi-device scenarios.
Detail on fail: "No conflict resolution strategy detected. If user edits offline and remote changes occur simultaneously, one change could be lost."
Remediation: Implement field-level timestamps and merge strategy:
interface Record {
id: string;
title: string;
titleUpdatedAt: number;
description: string;
descriptionUpdatedAt: number;
}
function mergeConflict(local: Record, remote: Record): Record {
// Compare timestamps for each field
// Take the most recently updated value
return {
id: local.id,
title: (local.titleUpdatedAt ?? 0) > (remote.titleUpdatedAt ?? 0)
? local.title
: remote.title,
titleUpdatedAt: Math.max(
local.titleUpdatedAt ?? 0,
remote.titleUpdatedAt ?? 0
),
description: (local.descriptionUpdatedAt ?? 0) > (remote.descriptionUpdatedAt ?? 0)
? local.description
: remote.description,
descriptionUpdatedAt: Math.max(
local.descriptionUpdatedAt ?? 0,
remote.descriptionUpdatedAt ?? 0
),
};
}
async function syncRecord(recordId: string) {
const local = await getLocalRecord(recordId);
const remote = await fetchRemoteRecord(recordId);
if (local && remote) {
// Merge conflict
const merged = mergeConflict(local, remote);
await saveLocalRecord(merged);
// Optionally notify user
showNotification('Data merged from multiple edits');
} else if (local && !remote) {
// Only local, upload it
await uploadRecord(local);
} else if (remote && !local) {
// Only remote, save it
await saveLocalRecord(remote);
}
}