Repository navigation
v0.5.0 — Tier-1 collaboration APIs
[0.5.0] — 2026-05-01
Added
All the "Tier 1" features — the minimum surface for a collaborative editor
product — are now exposed.
Rich text (Peritext).
LoroEx.text_mark/6,text_unmark/5— apply and remove formatting marks
(bold, italic, link, color, …) over Unicode ranges.LoroEx.text_to_delta/2,text_apply_delta/3— Quill-compatible deltas
for interop with TipTap, ProseMirror, and CodeMirror rich-text editors.LoroEx.text_get_richtext_value/2— decoded rich-text segments with
per-run attributes.LoroEx.text_len/3— length in:unicode | :utf8 | :utf16units.LoroEx.text_convert_pos/5— convert an index between unit systems.
Useful at the browser/server boundary (UTF-16 ↔ Unicode).
Cursors (stable positions).
LoroEx.text_get_cursor/4,list_get_cursor/4— produce an opaque
binary that tracks a position across concurrent edits.LoroEx.cursor_resolve/2— resolve a cursor to its current
{pos, side}. Returns{:error, {:history_cleared | :container_deleted | :id_not_found, _}}when the anchor is gone.- New atoms:
:left | :middle | :right,:unicode | :utf8 | :utf16.
Undo / redo.
- New module
LoroEx.UndoManagerwithnew/1,undo/1,redo/1,
can_undo/1,can_redo/1,record_new_checkpoint/1,
set_max_undo_steps/2,set_merge_interval/2. Local-only per Loro's
semantics: only undoes operations from the bound peer.
Ephemeral / awareness (presence).
- New module
LoroEx.Presencewrapping Loro'sEphemeralStore. new/1,set/3,get/2,delete/2,keys/1,get_all_states/1,
encode/2,encode_all/1,apply/2,remove_outdated/1,
subscribe/2. Subscription callback delivers
{:loro_ephemeral, ref, bytes}— bytes are ready to feed into
apply/2on a peer store.
Structured diff subscriptions.
LoroEx.subscribe_container/3— subscribepidto diffs on one
container (root name or nested ContainerID).LoroEx.subscribe_root/2— subscribe to diffs across the whole doc.- Both deliver
{:loro_diff, ref, events_json_binary}with a structured
diff payload (text ops, map updates, list ops, tree actions).
Nested containers.
LoroEx.map_insert_container/4,list_insert_container/4— create a
new:text | :map | :list | :movable_listnested inside a parent
container. Returns the new container's serialized id.LoroEx.tree_get_meta/3— return the id of a tree node's metadata
map. Pass it tomap_set/4/map_get_json/3to store per-node data
(title, icon, props).- All container-taking NIFs accept serialized
ContainerIDstrings in
addition to root names.
Changed
- Container-id resolution is unified: functions try
ContainerID::try_from
first and fall back to root-name lookup. No API break.
New error reasons
:invalid_cursor, :invalid_delta, :invalid_container_kind,
:history_cleared, :id_not_found, :ephemeral_apply_failed.
Tested
Sixteen new :nif tests across seven describe blocks: text marks,
length helpers, cursors, undo manager, presence, structured diff
subscriptions, nested containers, plus one end-to-end cross-feature
test (edit + mark + cursor survives concurrent insert + undo + presence)
that exercises every Tier-1 surface together.
39 :nif tests total, all passing.
Documentation
- Every public function in
LoroEx,LoroEx.UndoManager, and
LoroEx.Presencenow has a@docblock with Purpose / Use cases /
Example / Errors / See also sections. - New topic guides under
docs/guides/:
Getting started,
Rich text,
Sync & persistence,
Presence & cursors,
Tree & blocks,
Undo. mix.exswires all guides into ExDoc with sidebar groups.
mix docsnow builds without warnings.
Build
loropinned exactly at=1.12.0inCargo.toml(was"1.10"
caret range; the lockfile was already resolving to 1.12.0).rust-versionbumped to 1.91 inCargo.tomlto match the Rustler
0.37 MSRV stated in the README andrust-toolchain.toml.
Behavior notes (not a break in practice)
container_idstrings starting with"cid:"are now parsed as
serializedContainerIDs rather than treated as root-container
names. If you were using a literal"cid:…"string as a root name
in v0.4.0 (extremely unusual), rename it. Any other root name
("body","settings", etc.) is unaffected.
Known limitations
- Subscriptions must be unsubscribed explicitly. Due to an
Arc cycle between the subscription handle and its delivery
closure, dropping a subscription reference without calling
unsubscribe/1leaks the subscription (and its captured doc ref)
until BEAM exits. Always pair everysubscribe/*with an
unsubscribe/1at shutdown. Fix planned for 0.5.1 via a global
subscription registry. text_len/3with an unknown unit atom returns
{:error, {:invalid_value, _}}instead of raising
FunctionClauseError(added in this release). Previously unknown
units crashed the caller.