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.

src/notebook.tsts
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.

src/resolve-conflict.tsts
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