Skip to content

Wit Components #881

@sevki

Description

@sevki

Hey folks,

I've been trying to add wit support for loro.

Since loro is split between the loro_internal and loro crates, generating the facade from a wit file works quite well.

for instance the following

wit file
package loro:crdt@1.0.0;

/// Core Loro CRDT component
world loro {
    export document;
    export events;
    export errors;
}

/// Document management interface
interface document {
    use types.{loro-value, container-id, container-type, export-mode, value-or-container};
    use errors.{error};

    /// Document resource handle
    resource document {
        /// Create a new Loro document
        constructor();

        /// Set the peer ID for this document
        set-peer-id: func(peer-id: u64) -> result<_, error>;

        /// Commit pending changes
        commit: func(doc: borrow<document>) -> result<_, error>;

        /// Export document in specified mode
        export-doc: func(mode: export-mode) -> result<list<u8>, error>;

        /// Import updates into document
        import-doc: func(data: list<u8>) -> result<_, error>;

        /// Get document state as JSON string
        to-json: func() -> result<string, error>;

        /// Fork document at current version
        fork: func() -> result<document, error>;

        /// Get a text container
        get-text: func(id: string) -> result<text, error>;

        /// Get a list container
        get-list: func(id: string) -> result<%list, error>;

        /// Get a map container
        get-map: func(id: string) -> result<map, error>;

        /// Get a tree container
        get-tree: func(id: string) -> result<tree, error>;

        /// Get a movable list container
        get-movable-list: func(id: string) -> result<movable-list, error>;

        /// Get a counter container
        get-counter: func(id: string) -> result<counter, error>;
    }

    enum postype {
        /// The index is based on the length of the text in bytes(UTF-8).
        bytes,
        /// The index is based on the length of the text in Unicode Code Points.
        unicode,
        /// The index is based on the length of the text in UTF-16 code units.
        utf16,
        /// The index is based on the length of the text in events.
        /// It is determined by the `wasm` feature.
        event,
        /// The index is based on the entity index.
        entity,
    }

    /// Text container resource handle
    resource text {
        /// Insert text at position
        insert: func(pos: u32, text: string, postype: postype) -> result<_, error>;

        /// Delete text at position
        delete: func(pos: u32, len: u32) -> result<_, error>;

        /// Get text content
        to-string: func() -> result<string, error>;

        /// Get text length
        len: func() -> result<u32, error>;
    }

    /// List container resource handle
    resource %list {
        /// Insert value at index
        insert: func(index: u32, value: loro-value) -> result<_, error>;

        /// Delete elements at index
        delete: func(index: u32, count: u32) -> result<_, error>;

        /// Get value at index
        get: func(index: u32) -> result<option<loro-value>, error>;

        /// Get list length
        len: func() -> result<u32, error>;

        /// Push value to end
        push: func(value: loro-value) -> result<_, error>;
    }

    /// Map container resource handle
    resource map {
        /// Insert key-value pair
        insert: func(key: string, value: loro-value) -> result<_, error>;

        /// Delete key
        delete: func(key: string) -> result<_, error>;

        /// Get value by key
        get: func(key: string) -> result<option<loro-value>, error>;

        /// Check if key exists
        contains: func(key: string) -> result<bool, error>;

        /// Get all keys
        keys: func() -> result<list<string>, error>;
    }

    /// Tree container resource handle
    resource tree {
        /// Create a new tree node
        create: func(parent: option<borrow<tree-node-id>>) -> result<tree-node-id, error>;

        /// Move node to new parent
        move-node: func(node: borrow<tree-node-id>, parent: option<borrow<tree-node-id>>) -> result<_, error>;

        /// Delete node
        delete: func(node: borrow<tree-node-id>) -> result<_, error>;
    }

    /// Movable list container resource handle
    resource movable-list {
        /// Insert value at index
        insert: func(index: u32, value: loro-value) -> result<_, error>;

        /// Delete elements at index
        delete: func(index: u32, count: u32) -> result<_, error>;

        /// Move element from one position to another
        move-item: func(from-index: u32, to-index: u32) -> result<_, error>;

        /// Get value at index
        get: func(index: u32) -> result<option<loro-value>, error>;

        /// Get list length
        len: func() -> result<u32, error>;
    }

    /// Counter container resource handle
    resource counter {
        /// Increment the counter by the given value
        increment: func(value: f64) -> result<_, error>;

        /// Decrement the counter by the given value
        decrement: func(value: f64) -> result<_, error>;

        /// Get the current value of the counter
        get-value: func() -> result<f64, error>;
    }

    /// Tree node identifier resource
    resource tree-node-id;

    /// Container variant type
    variant container {
        text(text),
        %list(%list),
        map(map),
        tree(tree),
        movable-list(movable-list),
        counter(counter),
    }
}

/// Event types for document change notifications
interface events {
    use types.{loro-value, container-id, value-or-container, tree-id};

    /// How an event was triggered
    enum event-trigger-kind {
        /// Triggered by a local change
        local,
        /// Triggered by importing remote changes
        %import,
        /// Triggered by a checkout operation
        checkout,
    }

    /// A diff event containing all changes from an operation
    record diff-event {
        /// How the event was triggered
        triggered-by: event-trigger-kind,
        /// The origin string of the event
        origin: string,
        /// The current target container (if any)
        current-target: option<container-id>,
        /// List of container diffs
        events: list<container-diff>,
    }

    /// A diff for a specific container
    record container-diff {
        /// The target container id
        target: container-id,
        /// The path from root to this container
        path: list<path-item>,
        /// Whether this is from an unknown container
        is-unknown: bool,
        /// The actual diff content
        diff: diff,
    }

    /// A path item representing a step in the container path
    record path-item {
        /// The container ID at this path step
        container: container-id,
        /// The index within the container
        index: index,
    }

    /// Index type for path navigation
    variant index {
        /// Key index for maps
        key(string),
        /// Numeric index for lists/text
        seq(u32),
    }

    /// The main diff variant type
    variant diff {
        /// List diff operations
        %list(list<list-diff-item>),
        /// Text diff operations
        text(list<text-delta>),
        /// Map diff operations
        map(map-delta),
        /// Tree diff operations
        tree(tree-diff),
        /// Counter diff (the change in value)
        counter(f64),
        /// Unknown container diff
        unknown,
    }

    /// A list diff item - insert, delete, or retain
    variant list-diff-item {
        /// Insert new elements
        insert(list-diff-insert),
        /// Delete elements
        delete(list-diff-delete),
        /// Retain elements (no change)
        retain(list-diff-retain),
    }

    /// List insert operation
    record list-diff-insert {
        /// Values to insert
        insert: list<value-or-container>,
        /// Whether this insert is from a move operation
        is-move: bool,
    }

    /// List delete operation
    record list-diff-delete {
        /// Number of elements to delete
        delete: u32,
    }

    /// List retain operation
    record list-diff-retain {
        /// Number of elements to retain
        retain: u32,
    }

    /// Text delta operations
    variant text-delta {
        /// Retain text
        retain(text-retain),
        /// Insert text
        insert(text-insert),
        /// Delete text
        delete(text-delete),
    }

    /// Text retain operation
    record text-retain {
        /// Number of characters to retain
        retain: u32,
        /// Optional attributes for the retained range
        attributes: option<list<text-attribute>>,
    }

    /// Text insert operation
    record text-insert {
        /// The text to insert
        insert: string,
        /// Optional attributes for the inserted text
        attributes: option<list<text-attribute>>,
    }

    /// Text delete operation
    record text-delete {
        /// Number of characters to delete
        delete: u32,
    }

    /// A text attribute key-value pair
    record text-attribute {
        /// The attribute key
        key: string,
        /// The attribute value
        value: loro-value,
    }

    /// Map delta containing updated entries
    record map-delta {
        /// List of updated key-value pairs
        updated: list<map-delta-entry>,
    }

    /// A map delta entry
    record map-delta-entry {
        /// The key that was updated
        key: string,
        /// The new value (None if deleted)
        value: option<value-or-container>,
    }

    /// Tree diff containing tree operations
    record tree-diff {
        /// List of tree diff items
        diff: list<tree-diff-item>,
    }

    /// A single tree diff item
    record tree-diff-item {
        /// The target node being modified
        target: tree-id,
        /// The action performed
        action: tree-external-diff,
    }

    /// Tree external diff action
    variant tree-external-diff {
        /// Create a new node
        create(tree-create-diff),
        /// Move an existing node
        move(tree-move-diff),
        /// Delete a node
        delete(tree-delete-diff),
    }

    /// Tree create diff details
    record tree-create-diff {
        /// The parent of the new node
        parent: tree-parent-id,
        /// Index among siblings
        index: u32,
        /// Fractional index for ordering
        position: string,
    }

    /// Tree move diff details
    record tree-move-diff {
        /// The new parent
        parent: tree-parent-id,
        /// New index among siblings
        index: u32,
        /// New fractional index position
        position: string,
        /// The old parent before the move
        old-parent: tree-parent-id,
        /// The old index before the move
        old-index: u32,
    }

    /// Tree delete diff details
    record tree-delete-diff {
        /// The parent the node was deleted from
        old-parent: tree-parent-id,
        /// The index the node had before deletion
        old-index: u32,
    }

    /// Tree parent identifier
    variant tree-parent-id {
        /// A regular node as parent
        node(tree-id),
        /// The root of the tree (no parent)
        root,
        /// The deleted nodes root
        deleted,
        /// Node doesn't exist yet (for creation)
        unexist,
    }

    /// A batch of diffs for multiple containers
    record diff-batch {
        /// List of container diffs in order
        entries: list<diff-batch-entry>,
    }

    /// A single entry in a diff batch
    record diff-batch-entry {
        /// The container ID
        container-id: container-id,
        /// The diff for this container
        diff: diff,
    }
}

/// Core types used across interfaces
interface types {
    /// Loro value types
    variant loro-value {
        /// Null value
        null,
        /// Boolean value
        %bool(bool),
        /// 64-bit signed integer
        i64(s64),
        /// 64-bit floating point
        %f64(f64),
        /// UTF-8 string
        %string(string),
        /// Binary data
        binary(list<u8>),
        /// Container reference
        container(container-id),
    }

    /// Value or container - used in diffs and list operations
    /// Uses container-id instead of container resource to avoid circular dependency
    variant value-or-container {
        /// A primitive value
        value(loro-value),
        /// A container reference (by ID, resolve via document)
        container(container-id),
    }

    /// Container identifier
    record container-id {
        /// Container ID string
        id: string,
        /// Type of container
        container-type: container-type,
    }

    /// Tree node identifier
    record tree-id {
        /// Peer ID that created this node
        peer: u64,
        /// Counter value for this node
        counter: s32,
    }

    /// Container types
    enum container-type {
        /// Text container
        text,
        /// List container
        %list,
        /// Map container
        %map,
        /// Tree container
        tree,
        /// Movable list container
        movable-list,
        /// Counter container
        counter,
    }

    /// Export modes for document serialization
    enum export-mode {
        /// Full document state with compressed history
        snapshot,
        /// All updates since document creation
        updates,
        /// Incremental updates from a specific version
        updates-from-version,
    }
}

can be implemented with

rust file
use loro_common::{ContainerID, ContainerType, LoroEncodeError, LoroValue, TreeID};
use loro_internal::cursor::PosType;
use loro_internal::loro::ExportMode as InternalExportMode;
use loro_internal::{
    handler::counter::CounterHandler, ListHandler, LoroDoc, MapHandler, MovableListHandler,
    TextHandler, ToJson, TreeHandler, TreeParentId,
};

use crate::bindings::exports::loro::crdt::document::{
    Counter, Document, DocumentBorrow, Error, ExportMode, Guest, GuestCounter, GuestDocument,
    GuestList, GuestMap, GuestMovableList, GuestText, GuestTree, GuestTreeNodeId, List, Map,
    MovableList, Postype, Text, Tree, TreeNodeId,
};
use crate::bindings::loro::crdt::types::{
    ContainerId as WitContainerId, ContainerType as WitContainerType, LoroValue as WitValue,
};
use crate::error::map_error;

/// Empty enum to implement Guest trait on
pub enum Loro {}

impl Guest for Loro {
    type Document = LoroDoc;

    type Text = TextHandler;

    type List = ListHandler;

    type Map = MapHandler;

    type Tree = TreeHandler;

    type MovableList = MovableListHandler;

    type Counter = CounterHandler;

    type TreeNodeId = TreeID;
}

impl GuestDocument for LoroDoc {
    fn new() -> Self {
        LoroDoc::new_auto_commit()
    }

    fn set_peer_id(&self, peer_id: u64) -> Result<(), Error> {
        self.set_peer_id(peer_id).map_err(map_error)
    }

    fn commit(&self, _doc: DocumentBorrow<'_>) -> Result<(), Error> {
        self.commit_then_renew();
        Ok(())
    }

    fn export_doc(&self, mode: ExportMode) -> Result<Vec<u8>, Error> {
        let mode = match mode {
            ExportMode::Snapshot => InternalExportMode::snapshot(),
            ExportMode::Updates => InternalExportMode::all_updates(),
            ExportMode::UpdatesFromVersion => InternalExportMode::all_updates(),
        };

        self.export(mode).map_err(map_encode_error)
    }

    fn import_doc(&self, data: Vec<u8>) -> Result<(), Error> {
        self.import(&data).map(|_| ()).map_err(map_error)
    }

    fn to_json(&self) -> Result<String, Error> {
        Ok(self.get_deep_value().to_json())
    }

    fn fork(&self) -> Result<Document, Error> {
        Ok(Document::new(self.fork()))
    }

    fn get_text(&self, id: String) -> Result<Text, Error> {
        Ok(Text::new(self.get_text(id)))
    }

    fn get_list(&self, id: String) -> Result<List, Error> {
        Ok(List::new(self.get_list(id)))
    }

    fn get_map(&self, id: String) -> Result<Map, Error> {
        Ok(Map::new(self.get_map(id)))
    }

    fn get_tree(&self, id: String) -> Result<Tree, Error> {
        Ok(Tree::new(self.get_tree(id)))
    }

    fn get_movable_list(&self, id: String) -> Result<MovableList, Error> {
        Ok(MovableList::new(self.get_movable_list(id)))
    }

    fn get_counter(&self, id: String) -> Result<Counter, Error> {
        Ok(Counter::new(self.get_counter(id)))
    }
}

impl GuestText for TextHandler {
    fn insert(&self, pos: u32, text: String, postype: Postype) -> Result<(), Error> {
        self.insert(pos as usize, &text, PosType::from(postype))
            .map_err(map_error)
    }

    fn delete(&self, pos: u32, len: u32) -> Result<(), Error> {
        self.delete_utf16(pos as usize, len as usize)
            .map_err(map_error)
    }

    fn to_string(&self) -> Result<String, Error> {
        let value = self.get_richtext_value();
        if let LoroValue::String(s) = value {
            Ok(s.to_string())
        } else {
            Ok(value.to_json())
        }
    }

    fn len(&self) -> Result<u32, Error> {
        len_to_u32(self.len_utf16())
    }
}

impl GuestList for ListHandler {
    fn insert(&self, index: u32, value: WitValue) -> Result<(), Error> {
        let value = to_internal_value(value)?;
        self.insert(index as usize, value).map_err(map_error)
    }

    fn delete(&self, index: u32, count: u32) -> Result<(), Error> {
        self.delete(index as usize, count as usize)
            .map_err(map_error)
    }

    fn get(&self, index: u32) -> Result<Option<WitValue>, Error> {
        self.get(index as usize).map(to_wit_value).transpose()
    }

    fn len(&self) -> Result<u32, Error> {
        len_to_u32(self.len())
    }

    fn push(&self, value: WitValue) -> Result<(), Error> {
        let value = to_internal_value(value)?;
        self.push(value).map_err(map_error)
    }
}

impl GuestMap for MapHandler {
    fn insert(&self, key: String, value: WitValue) -> Result<(), Error> {
        let value = to_internal_value(value)?;
        self.insert(&key, value).map_err(map_error)
    }

    fn delete(&self, key: String) -> Result<(), Error> {
        self.delete(&key).map_err(map_error)
    }

    fn get(&self, key: String) -> Result<Option<WitValue>, Error> {
        self.get(&key).map(to_wit_value).transpose()
    }

    fn contains(&self, key: String) -> Result<bool, Error> {
        Ok(self.get(&key).is_some())
    }

    fn keys(&self) -> Result<Vec<String>, Error> {
        let mut keys = Vec::new();
        self.for_each(|k, _| keys.push(k.to_string()));
        Ok(keys)
    }
}

impl GuestMovableList for MovableListHandler {
    fn insert(&self, index: u32, value: WitValue) -> Result<(), Error> {
        let value = to_internal_value(value)?;
        self.insert(index as usize, value).map_err(map_error)
    }

    fn delete(&self, index: u32, count: u32) -> Result<(), Error> {
        self.delete(index as usize, count as usize)
            .map_err(map_error)
    }

    fn move_item(&self, from_index: u32, to_index: u32) -> Result<(), Error> {
        self.mov(from_index as usize, to_index as usize)
            .map_err(map_error)
    }

    fn get(&self, index: u32) -> Result<Option<WitValue>, Error> {
        self.get(index as usize).map(to_wit_value).transpose()
    }

    fn len(&self) -> Result<u32, Error> {
        len_to_u32(self.len())
    }
}

impl GuestTree for TreeHandler {
    fn create(
        &self,
        parent: Option<crate::bindings::exports::loro::crdt::document::TreeNodeIdBorrow<'_>>,
    ) -> Result<TreeNodeId, Error> {
        let parent_id = parent.map(|p| *p.get::<TreeID>());
        self.create(TreeParentId::from(parent_id))
            .map(TreeNodeId::new)
            .map_err(map_error)
    }

    fn move_node(
        &self,
        node: crate::bindings::exports::loro::crdt::document::TreeNodeIdBorrow<'_>,
        parent: Option<crate::bindings::exports::loro::crdt::document::TreeNodeIdBorrow<'_>>,
    ) -> Result<(), Error> {
        let node_id = *node.get::<TreeID>();
        let parent_id = parent.map(|p| *p.get::<TreeID>());
        self.mov(node_id, TreeParentId::from(parent_id))
            .map_err(map_error)
    }

    fn delete(
        &self,
        node: crate::bindings::exports::loro::crdt::document::TreeNodeIdBorrow<'_>,
    ) -> Result<(), Error> {
        self.delete(*node.get::<TreeID>()).map_err(map_error)
    }
}

impl GuestTreeNodeId for TreeID {}

impl GuestCounter for CounterHandler {
    fn increment(&self, value: f64) -> Result<(), Error> {
        CounterHandler::increment(self, value).map_err(map_error)
    }

    fn decrement(&self, value: f64) -> Result<(), Error> {
        CounterHandler::decrement(self, value).map_err(map_error)
    }

    fn get_value(&self) -> Result<f64, Error> {
        use loro_internal::HandlerTrait;
        Ok(HandlerTrait::get_value(self).into_double().unwrap_or(0.0))
    }
}

impl From<Postype> for PosType {
    fn from(value: Postype) -> Self {
        match value {
            Postype::Bytes => PosType::Bytes,
            Postype::Unicode => PosType::Unicode,
            Postype::Utf16 => PosType::Utf16,
            Postype::Event => PosType::Event,
            Postype::Entity => PosType::Entity,
        }
    }
}

fn to_internal_container_type(ty: WitContainerType) -> ContainerType {
    match ty {
        WitContainerType::Text => ContainerType::Text,
        WitContainerType::List => ContainerType::List,
        WitContainerType::Map => ContainerType::Map,
        WitContainerType::Tree => ContainerType::Tree,
        WitContainerType::MovableList => ContainerType::MovableList,
        WitContainerType::Counter => ContainerType::Counter,
    }
}

fn to_wit_container_type(ty: ContainerType) -> Result<WitContainerType, Error> {
    match ty {
        ContainerType::Text => Ok(WitContainerType::Text),
        ContainerType::List => Ok(WitContainerType::List),
        ContainerType::Map => Ok(WitContainerType::Map),
        ContainerType::Tree => Ok(WitContainerType::Tree),
        ContainerType::MovableList => Ok(WitContainerType::MovableList),
        ContainerType::Counter => Ok(WitContainerType::Counter),
        _ => Err(Error::NotImplemented(format!(
            "Unsupported container type for WIT mapping: {:?}",
            ty
        ))),
    }
}

fn to_internal_container_id(id: WitContainerId) -> Result<ContainerID, Error> {
    match ContainerID::try_from(id.id.as_str()) {
        Ok(cid) => Ok(cid),
        Err(_err) => {
            let container_type = to_internal_container_type(id.container_type);
            Ok(ContainerID::new_root(&id.id, container_type))
        }
    }
}

fn to_wit_container_id(id: &ContainerID) -> Result<WitContainerId, Error> {
    Ok(WitContainerId {
        id: format!("{}", id),
        container_type: to_wit_container_type(id.container_type())?,
    })
}

fn to_internal_value(value: WitValue) -> Result<LoroValue, Error> {
    match value {
        WitValue::Null => Ok(LoroValue::Null),
        WitValue::Bool(v) => Ok(LoroValue::Bool(v)),
        WitValue::I64(v) => Ok(LoroValue::I64(v)),
        WitValue::F64(v) => Ok(LoroValue::Double(v)),
        WitValue::String(v) => Ok(LoroValue::String(v.into())),
        WitValue::Binary(v) => Ok(LoroValue::Binary(v.into())),
        WitValue::Container(c) => to_internal_container_id(c).map(LoroValue::Container),
    }
}

fn to_wit_value(value: LoroValue) -> Result<WitValue, Error> {
    match value {
        LoroValue::Null => Ok(WitValue::Null),
        LoroValue::Bool(v) => Ok(WitValue::Bool(v)),
        LoroValue::Double(v) => Ok(WitValue::F64(v)),
        LoroValue::I64(v) => Ok(WitValue::I64(v)),
        LoroValue::Binary(v) => Ok(WitValue::Binary((*v).clone())),
        LoroValue::String(v) => Ok(WitValue::String(v.to_string())),
        LoroValue::Container(id) => Ok(WitValue::Container(to_wit_container_id(&id)?)),
        other => Err(Error::NotImplemented(format!(
            "Cannot convert LoroValue variant {:?} to WIT value",
            other
        ))),
    }
}

fn len_to_u32(len: usize) -> Result<u32, Error> {
    u32::try_from(len).map_err(|_| Error::ArgError("Length exceeds u32".into()))
}

fn map_encode_error(err: LoroEncodeError) -> Error {
    Error::Unknown(format!("Encode error: {err}"))
}

The public facade in loro maps so well to the types, I could tab tab auto complete all most of it, (which is why this looks like it was vibe coded, whoops, but the point about how mapping a wit generted interfaces to loro_internal types stands).

Now the actual problem I have is this bit of code here

[target.'cfg(all(target_arch = "wasm32", not(features = "wasm")))'.dependencies]
wasm-bindgen = "0.2.100"

which breaks building things. Would you folks be up for me sending some wit compat patches or would that be out of scope?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions