Skip to content

v0.5.0 — Tier-1 collaboration APIs

Choose a tag to compare

@bmalum bmalum released this 01 May 11:25
· 32 commits to main since this release
3ab29d1

[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 | :utf16 units.
  • 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.UndoManager with new/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.Presence wrapping Loro's EphemeralStore.
  • 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/2 on a peer store.

Structured diff subscriptions.

  • LoroEx.subscribe_container/3 — subscribe pid to 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_list nested 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 to map_set/4 / map_get_json/3 to store per-node data
    (title, icon, props).
  • All container-taking NIFs accept serialized ContainerID strings 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

Build

  • loro pinned exactly at =1.12.0 in Cargo.toml (was "1.10"
    caret range; the lockfile was already resolving to 1.12.0).
  • rust-version bumped to 1.91 in Cargo.toml to match the Rustler
    0.37 MSRV stated in the README and rust-toolchain.toml.

Behavior notes (not a break in practice)

  • container_id strings starting with "cid:" are now parsed as
    serialized ContainerIDs 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/1 leaks the subscription (and its captured doc ref)
    until BEAM exits. Always pair every subscribe/* with an
    unsubscribe/1 at shutdown. Fix planned for 0.5.1 via a global
    subscription registry.
  • text_len/3 with an unknown unit atom returns
    {:error, {:invalid_value, _}} instead of raising
    FunctionClauseError (added in this release). Previously unknown
    units crashed the caller.