lomo

moon add Lampese/lomo@0.0.5
Download zip
Author
Version
0.0.5
License
MIT
Last updated
6 months ago
Downloads
38

Dependencies

README

#Lomo

Lomo is a native MoonBit port of the Loro CRDT engine. It preserves Loro's container semantics and deterministic merge behavior while keeping the core embeddable: you own networking and storage, Lomo owns merges, update encoding, undo, and presence primitives.

#Feature coverage

  • Containers: Map, List, Text, MovableList, Tree, Counter (including nested containers).
  • Text: UTF-8/UTF-16 insert/delete/mark/unmark, deltas, diffs.
  • Transactions and commit metadata (message + origin).
  • Export/import: updates, snapshots, version vectors, frontiers.
  • Undo/redo with grouping and origin-prefix filters.
  • Subscriptions: root/container/local update/peer id/pre-commit hooks.
  • Ephemeral presence store with encode/apply.

#Example: Local edits, merge, snapshot, undo

let ensure_ok = (res : Result[Unit, @types.LoroError]) => match res {
Ok(_) => ()
Err(err) => fail("\{err}")
}

let doc1 = LoroDoc::new()
let doc2 = LoroDoc::new()
doc1.set_peer_id(1)
doc2.set_peer_id(2)

let list1 = doc1.get_list("todos")
let map1 = doc1.get_map("profile")
let text1 = doc1.get_text("note")

doc1.set_next_commit_origin(Some("client:alpha"))
doc1.set_next_commit_message(Some("seed profile"))
ensure_ok(doc1.begin_transaction())
ensure_ok(doc1.map_set(map1, "name", @types.LoroValue::String("Ada")))
ensure_ok(doc1.map_set(map1, "role", @types.LoroValue::String("writer")))
ensure_ok(doc1.list_push(list1, @types.LoroValue::String("draft")))
ensure_ok(doc1.text_insert(text1, 0, "Hello Lomo"))
ensure_ok(doc1.commit_transaction())

let list2 = doc2.get_list("todos")
let text2 = doc2.get_text("note")
ensure_ok(doc2.list_push(list2, @types.LoroValue::String("review")))
ensure_ok(doc2.text_insert(text2, 0, "Hey! "))

let bytes1 = match doc1.export_updates() { Ok(b) => b Err(e) => fail("\{e}") }
let bytes2 = match doc2.export_updates() { Ok(b) => b Err(e) => fail("\{e}") }
ensure_ok(doc1.import_updates(bytes2))
ensure_ok(doc2.import_updates(bytes1))

let merged1 = match doc1.text_to_string(text1) { Ok(v) => v Err(e) => fail("\{e}") }
let merged2 = match doc2.text_to_string(text2) { Ok(v) => v Err(e) => fail("\{e}") }
guard merged1 == merged2 else { fail("text did not converge") }

let snapshot = match doc1.export_bytes(@types.ExportMode::Snapshot) {
Ok(b) => b
Err(e) => fail("\{e}")
}
let doc3 = LoroDoc::new()
ensure_ok(doc3.import_bytes(snapshot))

let undo = UndoManager::new(doc1)
undo.add_exclude_origin_prefix("remote:")
ignore(undo.undo())

#Example: Presence (EphemeralStore)

let ensure_ok = (res : Result[Unit, @types.LoroError]) => match res {
Ok(_) => ()
Err(err) => fail("\{err}")
}

let presence1 = EphemeralStore::new(15_000)
let presence2 = EphemeralStore::new(15_000)

presence1.set(
"1", // peer id as string
@types.LoroValue::Map({ "cursor": @types.LoroValue::I64(12) }),
)

let payload = presence1.encode_all()
ensure_ok(presence2.apply(payload))

#Example: Tree structure and metadata

let ensure_ok = (res : Result[Unit, @types.LoroError]) => match res {
Ok(_) => ()
Err(err) => fail("\{err}")
}

let doc = LoroDoc::new()
let tree = doc.get_tree("outline")

let root = match doc.tree_create(tree, None) {
Ok(id) => id
Err(e) => fail("\{e}")
}
let a = match doc.tree_create(tree, Some(root)) {
Ok(id) => id
Err(e) => fail("\{e}")
}
let b = match doc.tree_create(tree, Some(root)) {
Ok(id) => id
Err(e) => fail("\{e}")
}

// Attach metadata to a node via its meta map container.
let root_meta = doc.tree_get_meta(root)
ensure_ok(doc.map_set(root_meta, "title", @types.LoroValue::String("Chapter 1")))

// Reorder: move b before a under the same parent.
ensure_ok(doc.tree_move_before(tree, b, a))

let tree_value = match doc.tree_get_value_with_meta(tree) {
Ok(v) => v
Err(e) => fail("\{e}")
}
ignore(tree_value)

#Example: RichText marks and deltas

let ensure_ok = (res : Result[Unit, @types.LoroError]) => match res {
Ok(_) => ()
Err(err) => fail("\{err}")
}

let doc = LoroDoc::new()
let text = doc.get_text("body")

ensure_ok(doc.text_insert(text, 0, "Hello rich text"))
ensure_ok(
doc.text_mark(
text,
0,
5,
"bold",
@types.LoroValue::Bool(true),
),
)

let delta = match doc.text_to_delta(text) {
Ok(v) => v
Err(e) => fail("\{e}")
}
ignore(delta)

#Example: Nested containers

let ensure_ok = (res : Result[Unit, @types.LoroError]) => match res {
Ok(_) => ()
Err(err) => fail("\{err}")
}

let doc = LoroDoc::new()
let root = doc.get_map("root")

let list_id = match doc.map_insert_container(
root,
"items",
@types.ContainerType::List,
) {
Ok(id) => id
Err(e) => fail("\{e}")
}
let items = @container.ListHandler::new(list_id)
ensure_ok(doc.list_push(items, @types.LoroValue::String("one")))

let note_id = match doc.list_insert_container(
items,
1,
@types.ContainerType::Text,
) {
Ok(id) => id
Err(e) => fail("\{e}")
}
let note = @container.TextHandler::new(note_id)
ensure_ok(doc.text_insert(note, 0, "embedded text"))

let deep = doc.get_deep_value()
ignore(deep)

#Tests

  • Tests live in lomo/lomo_test.mbt.
  • Run: moon test

#
EphemeralSubscriber

type EphemeralSubscriber = (EphemeralStoreEvent) -> Bool

#
LocalEphemeralCallback

type LocalEphemeralCallback = (Bytes) -> Bool

#
LocalUpdateCallback

type LocalUpdateCallback = (Bytes) -> Bool

#
PeerIdUpdateCallback

type PeerIdUpdateCallback = (UInt64) -> Bool

#
PreCommitCallback

type PreCommitCallback = (CommitMeta) -> Bool

#
Subscriber

type Subscriber = (EventBatch) -> Bool

#
CommitMeta

pub struct CommitMeta {
message : String?
origin : String?
timestamp : Int64
}

impl Show for CommitMeta

#
CommitMeta::message

fn CommitMeta::message(self : CommitMeta) -> String?

#
CommitMeta::new

fn CommitMeta::new(timestamp : Int64) -> CommitMeta

#
CommitMeta::origin

fn CommitMeta::origin(self : CommitMeta) -> String?

#
CommitMeta::set_message

fn CommitMeta::set_message(self : CommitMeta, message : String?) -> Unit

#
CommitMeta::set_origin

fn CommitMeta::set_origin(self : CommitMeta, origin : String?) -> Unit

#
CommitMeta::set_timestamp

fn CommitMeta::set_timestamp(self : CommitMeta, timestamp : Int64) -> Unit

#
CommitMeta::timestamp

fn CommitMeta::timestamp(self : CommitMeta) -> Int64

#
ContainerEvent

#
ContainerEvent::container

#
EphemeralEventTrigger

pub enum EphemeralEventTrigger {
Local
Import
Timeout
}

#
EphemeralLocalEntry

type EphemeralLocalEntry

#
EphemeralRecord

type EphemeralRecord

#
EphemeralStore

pub struct EphemeralStore {
timeout_ms : UInt64
next_sub_id : Int
states : Map[String, EphemeralRecord]
local_subs : Array[EphemeralLocalEntry]
subs : Array[EphemeralSubscriberEntry]
}

#
EphemeralStore::apply

fn EphemeralStore::apply(self : EphemeralStore, data : Bytes) -> Result[Unit,
LoroError
]

#
EphemeralStore::delete

fn EphemeralStore::delete(self : EphemeralStore, key : String) -> Unit

#
EphemeralStore::encode

fn EphemeralStore::encode(self : EphemeralStore, key : String) -> Bytes

#
EphemeralStore::encode_all

fn EphemeralStore::encode_all(self : EphemeralStore) -> Bytes

#
EphemeralStore::get

#
EphemeralStore::get_all_states

#
EphemeralStore::keys

fn EphemeralStore::keys(self : EphemeralStore) -> Array[String]

#
EphemeralStore::new

fn EphemeralStore::new(timeout_ms : UInt64) -> EphemeralStore

#
EphemeralStore::remove_outdated

fn EphemeralStore::remove_outdated(self : EphemeralStore) -> Unit

#
EphemeralStore::set

fn EphemeralStore::set(self : EphemeralStore, key : String, value :
LoroValue
) -> Unit

#
EphemeralStore::subscribe

fn EphemeralStore::subscribe(self : EphemeralStore, callback : (EphemeralStoreEvent) -> Bool) -> EphemeralSubscription

#
EphemeralStore::subscribe_local_updates

fn EphemeralStore::subscribe_local_updates(self : EphemeralStore, callback : (Bytes) -> Bool) -> EphemeralSubscription

#
EphemeralStoreEvent

pub struct EphemeralStoreEvent {
by : EphemeralEventTrigger
added : Array[String]
updated : Array[String]
removed : Array[String]
}

#
EphemeralStoreEvent::added

fn EphemeralStoreEvent::added(self : EphemeralStoreEvent) -> Array[String]

#
EphemeralStoreEvent::by

#
EphemeralStoreEvent::removed

fn EphemeralStoreEvent::removed(self : EphemeralStoreEvent) -> Array[String]

#
EphemeralStoreEvent::updated

fn EphemeralStoreEvent::updated(self : EphemeralStoreEvent) -> Array[String]

#
EphemeralSubscriberEntry

type EphemeralSubscriberEntry

#
EphemeralSubscription

pub struct EphemeralSubscription {
store : EphemeralStore
id : Int
}

#
EphemeralSubscription::unsubscribe

fn EphemeralSubscription::unsubscribe(self : EphemeralSubscription) -> Unit

#
EventBatch

pub struct EventBatch {
events : Array[ContainerEvent]
is_local : Bool
peer : UInt64
}

impl Show for EventBatch

#
EventBatch::events

fn EventBatch::events(self : EventBatch) -> Array[ContainerEvent]

#
EventBatch::is_local

fn EventBatch::is_local(self : EventBatch) -> Bool

#
EventBatch::peer

fn EventBatch::peer(self : EventBatch) -> UInt64

#
LocalUpdateEntry

type LocalUpdateEntry

#
LoroDoc

pub struct LoroDoc {
oplog :
OpLog

state :
DocState

arena :
SharedArena

text_style :
StyleConfigMap

peer : UInt64
counter : Int
lamport : UInt
next_sub_id : Int
root_subs : Array[SubscriberEntry]
container_subs : Map[
ContainerID
, Array[SubscriberEntry]]
local_update_subs : Array[LocalUpdateEntry]
peer_id_subs : Array[PeerIdEntry]
pre_commit_subs : Array[PreCommitEntry]
next_commit_msg : String?
next_commit_origin : String?
txn_active : Bool
txn_ops : Array[
Op
]
txn_deps :
Frontiers
?
txn_base_id :
ID
?
txn_base_lamport : UInt
}

#
LoroDoc::apply_local_content_with_id

fn LoroDoc::apply_local_content_with_id(self : LoroDoc, id :
ID
, lamport : UInt, container_id :
ContainerID
, content :
RawOpContent
) -> Result[Unit,
LoroError
]

#
LoroDoc::apply_map_set

#
LoroDoc::begin_transaction

fn LoroDoc::begin_transaction(self : LoroDoc) -> Result[Unit,
LoroError
]

#
LoroDoc::checkout

#
LoroDoc::commit_transaction

fn LoroDoc::commit_transaction(self : LoroDoc) -> Result[Unit,
LoroError
]

#
LoroDoc::config_default_text_style

fn LoroDoc::config_default_text_style(self : LoroDoc, text_style :
StyleConfig
?) -> Unit

#
LoroDoc::config_text_style

fn LoroDoc::config_text_style(self : LoroDoc, text_style :
StyleConfigMap
) -> Unit

#
LoroDoc::counter_add

fn LoroDoc::counter_add(self : LoroDoc, handler :
CounterHandler
, delta : Double) -> Result[Unit,
LoroError
]

#
LoroDoc::counter_inc

#
LoroDoc::counter_value

#
LoroDoc::export_bytes

#
LoroDoc::export_from

#
LoroDoc::export_loro_snapshot

fn LoroDoc::export_loro_snapshot(self : LoroDoc) -> Result[Bytes,
LoroError
]

#
LoroDoc::export_loro_updates

fn LoroDoc::export_loro_updates(self : LoroDoc) -> Result[Bytes,
LoroError
]

#
LoroDoc::export_updates

fn LoroDoc::export_updates(self : LoroDoc) -> Result[Bytes,
LoroError
]

#
LoroDoc::export_updates_from

#
LoroDoc::get_counter

fn LoroDoc::get_counter(_self : LoroDoc, name : String) ->
CounterHandler

#
LoroDoc::get_deep_value

#
LoroDoc::get_list

fn LoroDoc::get_list(_self : LoroDoc, name : String) ->
ListHandler

#
LoroDoc::get_map

fn LoroDoc::get_map(_self : LoroDoc, name : String) ->
MapHandler

#
LoroDoc::get_movable_list

fn LoroDoc::get_movable_list(_self : LoroDoc, name : String) ->
MovableListHandler

#
LoroDoc::get_text

fn LoroDoc::get_text(_self : LoroDoc, name : String) ->
TextHandler

#
LoroDoc::get_tree

fn LoroDoc::get_tree(_self : LoroDoc, name : String) ->
TreeHandler

#
LoroDoc::import_bytes

fn LoroDoc::import_bytes(self : LoroDoc, bytes : Bytes) -> Result[Unit,
LoroError
]

#
LoroDoc::import_updates

fn LoroDoc::import_updates(self : LoroDoc, bytes : Bytes) -> Result[Unit,
LoroError
]

#
LoroDoc::in_transaction

fn LoroDoc::in_transaction(self : LoroDoc) -> Bool

#
LoroDoc::list_delete

fn LoroDoc::list_delete(self : LoroDoc, handler :
ListHandler
, pos : Int, len : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::list_get_or_create_container

#
LoroDoc::list_insert

#
LoroDoc::map_delete

fn LoroDoc::map_delete(self : LoroDoc, handler :
MapHandler
, key : String) -> Result[Unit,
LoroError
]

#
LoroDoc::map_get_or_create_container

#
LoroDoc::movable_list_delete

fn LoroDoc::movable_list_delete(self : LoroDoc, handler :
MovableListHandler
, pos : Int, len : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::movable_list_insert

#
LoroDoc::movable_list_move

fn LoroDoc::movable_list_move(self : LoroDoc, handler :
MovableListHandler
, from : Int, to : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::movable_list_set

#
LoroDoc::new

fn LoroDoc::new() -> LoroDoc

#
LoroDoc::oplog_frontiers

#
LoroDoc::oplog_vv

#
LoroDoc::set_next_commit_message

fn LoroDoc::set_next_commit_message(self : LoroDoc, message : String?) -> Unit

#
LoroDoc::set_next_commit_origin

fn LoroDoc::set_next_commit_origin(self : LoroDoc, origin : String?) -> Unit

#
LoroDoc::set_peer_id

fn LoroDoc::set_peer_id(self : LoroDoc, peer : UInt64) -> Unit

#
LoroDoc::subscribe

fn LoroDoc::subscribe(self : LoroDoc, container_id :
ContainerID
, callback : (EventBatch) -> Bool) -> Subscription

#
LoroDoc::subscribe_local_update

fn LoroDoc::subscribe_local_update(self : LoroDoc, callback : (Bytes) -> Bool) -> Subscription

#
LoroDoc::subscribe_peer_id_change

fn LoroDoc::subscribe_peer_id_change(self : LoroDoc, callback : (UInt64) -> Bool) -> Subscription

#
LoroDoc::subscribe_pre_commit

fn LoroDoc::subscribe_pre_commit(self : LoroDoc, callback : (CommitMeta) -> Bool) -> Subscription

#
LoroDoc::subscribe_root

fn LoroDoc::subscribe_root(self : LoroDoc, callback : (EventBatch) -> Bool) -> Subscription

#
LoroDoc::text_delete

fn LoroDoc::text_delete(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::text_delete_utf16

fn LoroDoc::text_delete_utf16(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::text_delete_utf8

fn LoroDoc::text_delete_utf8(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int) -> Result[Unit,
LoroError
]

#
LoroDoc::text_get_richtext_value

#
LoroDoc::text_insert

fn LoroDoc::text_insert(self : LoroDoc, handler :
TextHandler
, pos : Int, text : String) -> Result[Unit,
LoroError
]

#
LoroDoc::text_insert_utf16

fn LoroDoc::text_insert_utf16(self : LoroDoc, handler :
TextHandler
, pos : Int, text : String) -> Result[Unit,
LoroError
]

#
LoroDoc::text_insert_utf8

fn LoroDoc::text_insert_utf8(self : LoroDoc, handler :
TextHandler
, pos : Int, text : String) -> Result[Unit,
LoroError
]

#
LoroDoc::text_mark

fn LoroDoc::text_mark(self : LoroDoc, handler :
TextHandler
, start : Int, end : Int, key : String, value :
LoroValue
) -> Result[Unit,
LoroError
]

#
LoroDoc::text_mark_utf16

fn LoroDoc::text_mark_utf16(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int, key : String, value :
LoroValue
) -> Result[Unit,
LoroError
]

#
LoroDoc::text_mark_utf8

fn LoroDoc::text_mark_utf8(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int, key : String, value :
LoroValue
) -> Result[Unit,
LoroError
]

#
LoroDoc::text_slice

fn LoroDoc::text_slice(self : LoroDoc, handler :
TextHandler
, start : Int, end : Int) -> Result[String,
LoroError
]

#
LoroDoc::text_slice_utf16

fn LoroDoc::text_slice_utf16(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int) -> Result[String,
LoroError
]

#
LoroDoc::text_slice_utf8

fn LoroDoc::text_slice_utf8(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int) -> Result[String,
LoroError
]

#
LoroDoc::text_to_string

#
LoroDoc::text_unmark

fn LoroDoc::text_unmark(self : LoroDoc, handler :
TextHandler
, start : Int, end : Int, key : String) -> Result[Unit,
LoroError
]

#
LoroDoc::text_unmark_utf16

fn LoroDoc::text_unmark_utf16(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int, key : String) -> Result[Unit,
LoroError
]

#
LoroDoc::text_unmark_utf8

fn LoroDoc::text_unmark_utf8(self : LoroDoc, handler :
TextHandler
, pos : Int, len : Int, key : String) -> Result[Unit,
LoroError
]

#
LoroDoc::tree_get_meta

#
LoroDoc::tree_get_value_with_meta

#
LoroDoc::tree_move_to

#
PeerIdEntry

type PeerIdEntry

#
PreCommitEntry

type PreCommitEntry

#
SubscriberEntry

type SubscriberEntry

#
Subscription

pub struct Subscription {
doc : LoroDoc
id : Int
}

#
Subscription::unsubscribe

fn Subscription::unsubscribe(self : Subscription) -> Unit

#
UndoManager

pub struct UndoManager {
doc : LoroDoc
peer_id : UInt64
undo_stack : Array[UndoStep]
redo_stack : Array[UndoStep]
seen_changes : Int
tracking_state :
DocState

ignore_change_ids : Array[
ID
]
exclude_origin_prefixes : Array[String]
max_undo_steps : Int
merge_interval_ms : Int64
group_active : Bool
group_ops : Array[UndoOp]
}

#
UndoManager::add_exclude_origin_prefix

fn UndoManager::add_exclude_origin_prefix(_self : UndoManager, _prefix : String) -> Unit

#
UndoManager::can_redo

fn UndoManager::can_redo(self : UndoManager) -> Bool

#
UndoManager::can_undo

fn UndoManager::can_undo(self : UndoManager) -> Bool

#
UndoManager::clear

fn UndoManager::clear(self : UndoManager) -> Unit

#
UndoManager::group_end

fn UndoManager::group_end(self : UndoManager) -> Unit

#
UndoManager::group_start

fn UndoManager::group_start(self : UndoManager) -> Result[Unit,
LoroError
]

#
UndoManager::new

fn UndoManager::new(doc : LoroDoc) -> UndoManager

#
UndoManager::peer

fn UndoManager::peer(self : UndoManager) -> UInt64

#
UndoManager::record_new_checkpoint

fn UndoManager::record_new_checkpoint(self : UndoManager) -> Result[Unit,
LoroError
]

#
UndoManager::redo

#
UndoManager::redo_count

fn UndoManager::redo_count(self : UndoManager) -> Int

#
UndoManager::set_max_undo_steps

fn UndoManager::set_max_undo_steps(self : UndoManager, steps : Int) -> Unit

#
UndoManager::set_merge_interval

fn UndoManager::set_merge_interval(self : UndoManager, interval : Int64) -> Unit

#
UndoManager::top_redo_value

#
UndoManager::top_undo_value

#
UndoManager::undo

#
UndoManager::undo_count

fn UndoManager::undo_count(self : UndoManager) -> Int

#
UndoOp

impl Show for UndoOp

#
UndoOp::content_len

fn UndoOp::content_len(self : UndoOp) -> Int

#
UndoStep

pub struct UndoStep {
ops : Array[UndoOp]
}

impl Show for UndoStep

#
alloc_id_and_lamport

fn alloc_id_and_lamport(doc : LoroDoc, len : Int) -> (
ID
, UInt)

#
awareness_fixture_multi

let awareness_fixture_multi : Bytes

#
awareness_fixture_peer_v1

let awareness_fixture_peer_v1 : Bytes

#
awareness_fixture_peer_v2

let awareness_fixture_peer_v2 : Bytes

#
awareness_fixture_simple

let awareness_fixture_simple : Bytes

#
loro_fixture_shallow_snapshot

let loro_fixture_shallow_snapshot : Bytes

#
loro_fixture_snapshot

let loro_fixture_snapshot : Bytes

#
loro_fixture_updates

let loro_fixture_updates : Bytes