examples/live-notes Public
id: conflict-strategy
type: concept
owner: justin
reviewed: 2026-10-13
component: ops
describes:
- src/ops.ts
- src/room.ts
- client/session.ts
Conflict strategy
Edits are operations, not new copies of the text. There are two kinds: insert some text at an
offset, and delete a number of characters at an offset. Offsets are UTF-16 code units, the unit
a textarea's selectionStart uses. Replacing a selection is a delete followed by an insert.
The room decides the order
Every op carries the version of the note it was written against. The note's room
([[note-room]]) is the only place ops are ordered ([[0001-single-sequencer-per-note]]). When an
op arrives written against version 41 and the room is at 43, the room takes ops 42 and 43 from
its log and moves the new op past each of them with transform(). It applies the result, saves
it as version 44, acks the sender and sends the transformed op to everyone else.
Because the room handles one message at a time, there is never a question of which op came first: the one with the lower version.
Transform
transform(op, other, otherFirst) rewrites op so it applies after other, when both were
written against the same text. The cases are few:
- Inserts before an op shift it right by the inserted length; deletes before it shift it left.
- Two inserts at the same offset: the one the room sequenced first ends up first. The room
passes
otherFirst = truefor logged ops; an editor passesfalsewhen it moves a remote op past its own pending edits. Both sides then put the same text first. - Overlapping deletes only delete what is still there.
- An insert strictly inside a range someone else deleted at the same time is dropped, and the delete grows to cover it ([[0002-drop-inserts-inside-concurrent-deletes]]).
The tests in test/ops.test.ts check every pair of random concurrent ops converges in either
order, and test/session.test.ts runs three editors against a room with messages delivered in
random interleavings.
The editor's side
client/session.ts keeps at most one op in flight. Edits made while it waits for the ack go in
a queue, folded together with compose() when they touch the same spot, so a fast typist sends
a word per round trip. When a remote op arrives, the session moves it past the in-flight op and
the queue, and moves those past it, the same way the room will. When the ack arrives, the next
queued op goes out written against the version in the ack.
When an editor is too far behind
The room keeps the last 200 ops. An op written against an older version, one from a version the room has not reached, or one that does not fit the text after transforming, gets a snapshot back instead of an ack. The editor shows the snapshot and drops its pending edits. In practice this happens after a long disconnect, and the page disables the textarea while it is offline so nothing is typed into the void.