Persistly uses optimistic concurrency with baseVersion, the last cloud branch revision your client accepted.
JavaScript Conflict Handling
Treat 409 conflict handling as normal integration behavior.
The JavaScript facade keeps local and cloud state separate on conflict. Your game decides whether the player uses cloud, keeps local, or decides later.
Facade Conflict
Use saveData and forceSyncData for one-save games.
The same conflict helpers work on the default autosave slot, so beginner code does not need slot keys.
import { PersistlyGameSaveStatus, PersistlyGameSaves } from "@persistlyapp/sdk";
await PersistlyGameSaves.shared.saveData(currentGameData);
const result = await PersistlyGameSaves.shared.forceSyncData();
if (result.status === PersistlyGameSaveStatus.Conflict) {
const save = await PersistlyGameSaves.shared.inspectData();
// Show local-vs-cloud UI in your game.
const choice = await askPlayerHowToRecover({
local: save.data,
cloud: save.lastCloudData,
});
if (choice === "use-cloud") {
await PersistlyGameSaves.shared.acceptCloudData();
}
if (choice === "keep-local") {
await PersistlyGameSaves.shared.overwriteCloudData();
}
if (choice === "decide-later") {
await PersistlyGameSaves.shared.keepLocalDataForLater();
}
}Advanced Runtime
Use PersistlyClient directly only for wrappers or custom runtime control.
Raw sync gives you baseVersion and cloud conflict branches directly, but normal games should prefer the facade first.
const ConflictRecoveryChoice = {
UseCloud: "use-cloud",
KeepLocal: "keep-local",
} as const;
const result = await client.syncAccountSlot({
accountId,
accountSessionToken,
slotId,
baseVersion: local.version,
slotInfo: local.slotInfo,
data: nextData
});
if (result.status === PersistlySyncStatus.Accepted) {
local = result.slot;
}
if (result.status === PersistlySyncStatus.Conflict) {
const cloudSlot = result.slot;
// Implement this in your game UI. Keep choices in constants instead of ad-hoc strings.
const playerChoice = await askPlayerHowToRecover({ cloudSlot, localDraft });
if (playerChoice === ConflictRecoveryChoice.UseCloud) {
local = cloudSlot;
}
if (playerChoice === ConflictRecoveryChoice.KeepLocal) {
const retry = await client.syncAccountSlot({
accountId,
accountSessionToken,
slotId,
baseVersion: cloudSlot.version,
slotInfo: localDraft.slotInfo,
data: localDraft.data,
});
local = retry.slot;
}
}Rules
These are the conflict semantics the SDK depends on.
The server returns explicit conflict semantics instead of silently overwriting data.
Conflict responses return product-shaped local/cloud branches so the game can retry from that version.