JOLT DATA SDK GUIDE · 05
Resolve Manual conflicts
Most applications should keep Jolt's automatic conflict defaults. Choose a Manual policy only when the product must show competing edits to a person or apply its own domain rule.
Override only the policy you need
This Notebook asks for Manual handling when two devices change the same field. The delete policy keeps its automatic default because it is not overridden.
import {
App,
Collection,
Field,
Read,
Schema,
UpdateConflict,
} from "jolt-sdk/data";
@Schema({ version: 1 })
export class Note {
@Field.string()
text!: string;
@Field.boolean()
pinned!: boolean;
}
export const Notes = Collection.create(Note, {
access: {
read: Read.OwnIdentity,
create: true,
update: true,
},
conflicts: {
update: UpdateConflict.Manual,
},
});
export const Notebook = App.create({
id: "manual-notebook.example",
name: "Manual Notebook",
namespace: "manual-notebook",
data: { notes: Notes },
});Manual does not turn every concurrent change into a conflict. Changes to
different top-level fields still combine automatically. State.Conflicted
appears only when the declared policy needs an application decision.
Create a conflict in a test
App.testWorld() can model two installations of the same identity without
running two daemons. Named devices change their local copies independently;
world.sync() exchanges their changes deterministically.
import { Notebook } from "./manual-conflicts";
export async function resolveConcurrentEdits(
resolution: "choose-workstation" | "combine",
) {
const world = Notebook.testWorld();
const workstation = world.device("alice.jolt", "workstation");
const laptop = world.device("alice.jolt", "laptop");
const created = await workstation.notes.create({ text: "Original", pinned: false });
await world.sync();
const workstationCopy = await workstation.notes.get(created.ref);
const laptopCopy = await laptop.notes.get(created.ref);
if (!workstationCopy.isPresent() || !laptopCopy.isPresent()) {
throw new Error("Expected both devices to have the note");
}
await workstationCopy.update({ text: "Workstation edit" });
await laptopCopy.update({ text: "Laptop edit" });
await world.sync();
const conflict = await workstation.notes.get(created.ref);
if (!conflict.isConflicted()) {
throw new Error("Expected a Manual conflict");
}
if (resolution === "combine") {
return conflict.resolve({ text: "Combined edit", pinned: false });
}
const workstationEdit = conflict.alternatives.find(alternative => (
alternative.isPresent() && alternative.value.text === "Workstation edit"
));
if (workstationEdit === undefined || !workstationEdit.isPresent()) {
throw new Error("Expected the workstation edit");
}
return conflict.choose(workstationEdit);
}The normal Item state check narrows the result to a ConflictItem.
alternatives then contains immutable Present or Deleted possibilities. Each
alternative has isPresent() and isDeleted() helpers before application code
reads its value or chooses it.
Choose or resolve
Use conflict.choose(alternative) when one exact alternative should win. Use
conflict.resolve(value) when the application creates a new combined
value. A custom value must pass the current Schema Class.
Both operations return a new immutable Item. Keep that returned Item just as
you would after an ordinary update. If another resolution wins first, the stale
Conflict Item throws ConflictError; read the Item again before deciding what
the interface should do.
Manual deletion is separate
Set conflicts: { delete: DeleteConflict.Manual } only when a concurrent
deletion and update also needs a product decision. The alternatives can then
include a Deleted alternative. Choosing that alternative keeps the deletion;
choosing a Present alternative keeps its value.
An application may override update handling, delete handling, or both. Any omitted policy retains the automatic default.
Keep this out of the beginner path
Manual resolution adds user-interface states and domain decisions. Chirp does
not need it: its Resource definitions keep automatic handling and never expose
State.Conflicted, alternatives, choose, or resolve to the React code.
Continue learning
- Use the normal lifecycle in Change an Item.
- Return to Data SDK fundamentals.
- Look up exact policy types in the API reference.