loro/crates/loro-wasm/src/lib.rs
2024-08-17 16:52:46 +08:00

4405 lines
141 KiB
Rust

//! Loro WASM bindings.
#![allow(non_snake_case)]
#![allow(clippy::empty_docs)]
#![warn(missing_docs)]
use convert::resolved_diff_to_js;
use js_sys::{Array, Object, Promise, Reflect, Uint8Array};
use loro_internal::{
change::Lamport,
configure::{StyleConfig, StyleConfigMap},
container::{richtext::ExpandType, ContainerID},
cursor::{self, Side},
encoding::ImportBlobMetadata,
event::Index,
handler::{
Handler, ListHandler, MapHandler, TextDelta, TextHandler, TreeHandler, ValueOrHandler,
},
id::{Counter, PeerID, TreeID, ID},
loro::CommitOptions,
obs::SubID,
undo::{UndoItemMeta, UndoOrRedo},
version::Frontiers,
ContainerType, DiffEvent, FxHashMap, HandlerTrait, JsonSchema, LoroDoc, LoroValue,
MovableListHandler, UndoManager as InnerUndoManager, VersionVector as InternalVersionVector,
};
use rle::HasLength;
use serde::{Deserialize, Serialize};
use std::{cell::RefCell, cmp::Ordering, rc::Rc, sync::Arc};
use wasm_bindgen::{__rt::IntoJsResult, prelude::*, throw_val};
use wasm_bindgen_derive::TryFromJsValue;
mod counter;
pub use counter::LoroCounter;
mod awareness;
mod log;
use crate::convert::{handler_to_js_value, js_to_container, js_to_cursor};
pub use awareness::AwarenessWasm;
mod convert;
#[wasm_bindgen(start)]
fn run() {
console_error_panic_hook::set_once();
}
/// Enable debug info of Loro
#[wasm_bindgen(js_name = setDebug)]
pub fn set_debug() {
tracing_wasm::set_as_global_default();
}
type JsResult<T> = Result<T, JsValue>;
/// The CRDTs document. Loro supports different CRDTs include [**List**](LoroList),
/// [**RichText**](LoroText), [**Map**](LoroMap) and [**Movable Tree**](LoroTree),
/// you could build all kind of applications by these.
///
/// @example
/// ```ts
/// import { Loro } import "loro-crdt"
///
/// const loro = new Loro();
/// const text = loro.getText("text");
/// const list = loro.getList("list");
/// const map = loro.getMap("Map");
/// const tree = loro.getTree("tree");
/// ```
///
// When FinalizationRegistry is unavailable, it's the users' responsibility to free the document.
#[wasm_bindgen]
pub struct Loro(Arc<LoroDoc>);
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "number | bigint | `${number}`")]
pub type JsIntoPeerID;
#[wasm_bindgen(typescript_type = "PeerID")]
pub type JsStrPeerID;
#[wasm_bindgen(typescript_type = "ContainerID")]
pub type JsContainerID;
#[wasm_bindgen(typescript_type = "ContainerID | string")]
pub type JsIntoContainerID;
#[wasm_bindgen(typescript_type = "Transaction | Loro")]
pub type JsTransaction;
#[wasm_bindgen(typescript_type = "string | undefined")]
pub type JsOrigin;
#[wasm_bindgen(typescript_type = "{ peer: PeerID, counter: number }")]
pub type JsID;
#[wasm_bindgen(typescript_type = "{ peer: PeerID, counter: number }[]")]
pub type JsIDs;
#[wasm_bindgen(typescript_type = "{ start: number, end: number }")]
pub type JsRange;
#[wasm_bindgen(typescript_type = "number|bool|string|null")]
pub type JsMarkValue;
#[wasm_bindgen(typescript_type = "TreeID")]
pub type JsTreeID;
#[wasm_bindgen(typescript_type = "TreeID | undefined")]
pub type JsParentTreeID;
#[wasm_bindgen(typescript_type = "LoroTreeNode | undefined")]
pub type JsTreeNodeOrUndefined;
#[wasm_bindgen(typescript_type = "string | undefined")]
pub type JsPositionOrUndefined;
#[wasm_bindgen(typescript_type = "Delta<string>[]")]
pub type JsStringDelta;
#[wasm_bindgen(typescript_type = "Map<PeerID, number>")]
pub type JsVersionVectorMap;
#[wasm_bindgen(typescript_type = "Map<PeerID, Change[]>")]
pub type JsChanges;
#[wasm_bindgen(typescript_type = "Change")]
pub type JsChange;
#[wasm_bindgen(typescript_type = "Change | undefined")]
pub type JsChangeOrUndefined;
#[wasm_bindgen(
typescript_type = "Map<PeerID, number> | Uint8Array | VersionVector | undefined | null"
)]
pub type JsIntoVersionVector;
#[wasm_bindgen(typescript_type = "Container")]
pub type JsContainer;
#[wasm_bindgen(typescript_type = "Value")]
pub type JsLoroValue;
#[wasm_bindgen(typescript_type = "Value | Container")]
pub type JsValueOrContainer;
#[wasm_bindgen(typescript_type = "Value | Container | undefined")]
pub type JsValueOrContainerOrUndefined;
#[wasm_bindgen(typescript_type = "Container | undefined")]
pub type JsContainerOrUndefined;
#[wasm_bindgen(typescript_type = "LoroText | undefined")]
pub type JsLoroTextOrUndefined;
#[wasm_bindgen(typescript_type = "LoroMap | undefined")]
pub type JsLoroMapOrUndefined;
#[wasm_bindgen(typescript_type = "LoroList | undefined")]
pub type JsLoroListOrUndefined;
#[wasm_bindgen(typescript_type = "LoroTree | undefined")]
pub type JsLoroTreeOrUndefined;
#[wasm_bindgen(typescript_type = "[string, Value | Container]")]
pub type MapEntry;
#[wasm_bindgen(typescript_type = "{[key: string]: { expand: 'before'|'after'|'none'|'both' }}")]
pub type JsTextStyles;
#[wasm_bindgen(typescript_type = "Delta<string>[]")]
pub type JsDelta;
#[wasm_bindgen(typescript_type = "-1 | 1 | 0 | undefined")]
pub type JsPartialOrd;
#[wasm_bindgen(typescript_type = "'Tree'|'Map'|'List'|'Text'")]
pub type JsContainerKind;
#[wasm_bindgen(typescript_type = "'Text'")]
pub type JsTextStr;
#[wasm_bindgen(typescript_type = "'Tree'")]
pub type JsTreeStr;
#[wasm_bindgen(typescript_type = "'Map'")]
pub type JsMapStr;
#[wasm_bindgen(typescript_type = "'List'")]
pub type JsListStr;
#[wasm_bindgen(typescript_type = "'MovableList'")]
pub type JsMovableListStr;
#[wasm_bindgen(typescript_type = "ImportBlobMetadata")]
pub type JsImportBlobMetadata;
#[wasm_bindgen(typescript_type = "Side")]
pub type JsSide;
#[wasm_bindgen(typescript_type = "{ update?: Cursor, offset: number, side: Side }")]
pub type JsCursorQueryAns;
#[wasm_bindgen(typescript_type = "UndoConfig")]
pub type JsUndoConfig;
#[wasm_bindgen(typescript_type = "JsonSchema")]
pub type JsJsonSchema;
#[wasm_bindgen(typescript_type = "string | JsonSchema")]
pub type JsJsonSchemaOrString;
}
mod observer {
use std::thread::ThreadId;
use wasm_bindgen::JsValue;
use crate::JsResult;
/// We need to wrap the observer function in a struct so that we can implement Send for it.
/// But it's not Send essentially, so we need to check it manually in runtime.
#[derive(Clone)]
pub(crate) struct Observer {
f: js_sys::Function,
thread: ThreadId,
}
impl Observer {
pub fn new(f: js_sys::Function) -> Self {
Self {
f,
thread: std::thread::current().id(),
}
}
pub fn call1(&self, arg: &JsValue) -> JsResult<JsValue> {
if std::thread::current().id() == self.thread {
self.f.call1(&JsValue::NULL, arg)
} else {
Err(JsValue::from_str("Observer called from different thread"))
}
}
pub fn call2(&self, arg1: &JsValue, arg2: &JsValue) -> JsResult<JsValue> {
if std::thread::current().id() == self.thread {
self.f.call2(&JsValue::NULL, arg1, arg2)
} else {
Err(JsValue::from_str("Observer called from different thread"))
}
}
pub fn call3(&self, arg1: &JsValue, arg2: &JsValue, arg3: &JsValue) -> JsResult<JsValue> {
if std::thread::current().id() == self.thread {
self.f.call3(&JsValue::NULL, arg1, arg2, arg3)
} else {
Err(JsValue::from_str("Observer called from different thread"))
}
}
}
unsafe impl Send for Observer {}
unsafe impl Sync for Observer {}
}
fn ids_to_frontiers(ids: Vec<JsID>) -> JsResult<Frontiers> {
let mut frontiers = Frontiers::default();
for id in ids {
let id = js_id_to_id(id)?;
frontiers.push(id);
}
Ok(frontiers)
}
fn id_to_js(id: &ID) -> JsValue {
let obj = Object::new();
Reflect::set(&obj, &"peer".into(), &id.peer.to_string().into()).unwrap();
Reflect::set(&obj, &"counter".into(), &id.counter.into()).unwrap();
let value: JsValue = obj.into_js_result().unwrap();
value
}
fn js_id_to_id(id: JsID) -> Result<ID, JsValue> {
let peer = js_peer_to_peer(Reflect::get(&id, &"peer".into())?)?;
let counter = Reflect::get(&id, &"counter".into())?.as_f64().unwrap() as Counter;
let id = ID::new(peer, counter);
Ok(id)
}
fn frontiers_to_ids(frontiers: &Frontiers) -> JsIDs {
let js_arr = Array::new();
for id in frontiers.iter() {
let value = id_to_js(id);
js_arr.push(&value);
}
let value: JsValue = js_arr.into();
value.into()
}
fn js_value_to_container_id(
cid: &JsIntoContainerID,
kind: ContainerType,
) -> Result<ContainerID, JsValue> {
if !cid.is_string() {
return Err(JsValue::from_str(&format!(
"ContainerID must be a string, but found {}",
cid.js_typeof().as_string().unwrap(),
)));
}
let s = cid.as_string().unwrap();
let cid = ContainerID::try_from(s.as_str())
.unwrap_or_else(|_| ContainerID::new_root(s.as_str(), kind));
Ok(cid)
}
#[derive(Debug, Clone, Serialize)]
struct StringID {
peer: String,
counter: Counter,
}
#[derive(Debug, Clone, Serialize)]
struct ChangeMeta {
lamport: Lamport,
length: u32,
peer: String,
counter: Counter,
deps: Vec<StringID>,
timestamp: f64,
}
impl ChangeMeta {
fn to_js(&self) -> JsValue {
let s = serde_wasm_bindgen::Serializer::new();
self.serialize(&s).unwrap()
}
}
#[wasm_bindgen]
impl Loro {
/// Create a new loro document.
///
/// New document will have random peer id.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
let doc = LoroDoc::new();
doc.start_auto_commit();
Self(Arc::new(doc))
}
/// Set whether to record the timestamp of each change. Default is `false`.
///
/// If enabled, the Unix timestamp will be recorded for each change automatically.
///
/// You can also set each timestamp manually when you commit a change.
/// The timestamp manually set will override the automatic one.
///
/// NOTE: Timestamps are forced to be in ascending order.
/// If you commit a new change with a timestamp that is less than the existing one,
/// the largest existing timestamp will be used instead.
#[wasm_bindgen(js_name = "setRecordTimestamp")]
pub fn set_record_timestamp(&self, auto_record: bool) {
self.0.set_record_timestamp(auto_record);
}
/// If two continuous local changes are within the interval, they will be merged into one change.
///
/// The default value is 1_000_000, the default unit is milliseconds.
#[wasm_bindgen(js_name = "setChangeMergeInterval")]
pub fn set_change_merge_interval(&self, interval: f64) {
self.0.set_change_merge_interval(interval as i64);
}
/// Set the jitter of the tree position(Fractional Index).
///
/// The jitter is used to avoid conflicts when multiple users are creating the node at the same position.
/// value 0 is default, which means no jitter, any value larger than 0 will enable jitter.
/// Generally speaking, jitter will affect the growth rate of document size.
#[wasm_bindgen(js_name = "setFractionalIndexJitter")]
pub fn set_fractional_index_jitter(&self, jitter: u8) {
self.0.set_fractional_index_jitter(jitter);
}
/// Set the rich text format configuration of the document.
///
/// You need to config it if you use rich text `mark` method.
/// Specifically, you need to config the `expand` property of each style.
///
/// Expand is used to specify the behavior of expanding when new text is inserted at the
/// beginning or end of the style.
///
/// You can specify the `expand` option to set the behavior when inserting text at the boundary of the range.
///
/// - `after`(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
/// - `before`: when inserting text right before the given range, the mark will be expanded to include the inserted text
/// - `none`: the mark will not be expanded to include the inserted text at the boundaries
/// - `both`: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
///
/// @example
/// ```ts
/// const doc = new Loro();
/// doc.configTextStyle({
/// bold: { expand: "after" },
/// link: { expand: "before" }
/// });
/// const text = doc.getText("text");
/// text.insert(0, "Hello World!");
/// text.mark({ start: 0, end: 5 }, "bold", true);
/// expect(text.toDelta()).toStrictEqual([
/// {
/// insert: "Hello",
/// attributes: {
/// bold: true,
/// },
/// },
/// {
/// insert: " World!",
/// },
/// ] as Delta<string>[]);
/// ```
#[wasm_bindgen(js_name = "configTextStyle")]
pub fn config_text_style(&self, styles: JsTextStyles) -> JsResult<()> {
let mut style_config = StyleConfigMap::new();
// read key value pair in styles
for key in Reflect::own_keys(&styles)?.iter() {
let value = Reflect::get(&styles, &key).unwrap();
let key = key.as_string().unwrap();
// Assert value is an object, otherwise throw an error with desc
if !value.is_object() {
return Err("Text style config format error".into());
}
// read expand value from value
let expand = Reflect::get(&value, &"expand".into()).expect("`expand` not specified");
let expand_str = expand.as_string().unwrap();
// read allowOverlap value from value
style_config.insert(
key.into(),
StyleConfig {
expand: ExpandType::try_from_str(&expand_str)
.expect("`expand` must be one of `none`, `start`, `end`, `both`"),
},
);
}
self.0.config_text_style(style_config);
Ok(())
}
/// Get a loro document from the snapshot.
///
/// @see You can check out what is the snapshot [here](#).
///
/// @example
/// ```ts
/// import { Loro } import "loro-crdt"
///
/// const bytes = /* The bytes encoded from other loro document *\/;
/// const loro = Loro.fromSnapshot(bytes);
/// ```
///
#[wasm_bindgen(js_name = "fromSnapshot")]
pub fn from_snapshot(snapshot: &[u8]) -> JsResult<Loro> {
let doc = LoroDoc::from_snapshot(snapshot)?;
doc.start_auto_commit();
Ok(Self(Arc::new(doc)))
}
/// Attach the document state to the latest known version.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// This method has the same effect as invoking `checkout_to_latest`.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// const frontiers = doc.frontiers();
/// text.insert(0, "Hello World!");
/// loro.checkout(frontiers);
/// // you need call `attach()` or `checkoutToLatest()` before changing the doc.
/// loro.attach();
/// text.insert(0, "Hi");
/// ```
pub fn attach(&mut self) {
self.0.attach();
}
/// `detached` indicates that the `DocState` is not synchronized with the latest version of `OpLog`.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// When `detached`, the document is not editable.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// const frontiers = doc.frontiers();
/// text.insert(0, "Hello World!");
/// console.log(doc.is_detached()); // false
/// loro.checkout(frontiers);
/// console.log(doc.is_detached()); // true
/// loro.attach();
/// console.log(doc.is_detached()); // false
/// ```
///
#[wasm_bindgen(js_name = "isDetached")]
pub fn is_detached(&self) -> bool {
self.0.is_detached()
}
/// Detach the document state from the latest known version.
///
/// After detaching, all import operations will be recorded in the `OpLog` without being applied to the `DocState`.
/// When `detached`, the document is not editable.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// doc.detach();
/// console.log(doc.is_detached()); // true
/// ```
pub fn detach(&self) {
self.0.detach()
}
/// Duplicate the document with a different PeerID
///
/// The time complexity and space complexity of this operation are both O(n),
pub fn fork(&self) -> Self {
Self(Arc::new(self.0.fork()))
}
/// Checkout the `DocState` to the latest version of `OpLog`.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// This has the same effect as `attach`.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// const frontiers = doc.frontiers();
/// text.insert(0, "Hello World!");
/// loro.checkout(frontiers);
/// // you need call `checkoutToLatest()` or `attach()` before changing the doc.
/// loro.checkoutToLatest();
/// text.insert(0, "Hi");
/// ```
#[wasm_bindgen(js_name = "checkoutToLatest")]
pub fn checkout_to_latest(&mut self) -> JsResult<()> {
self.0.checkout_to_latest();
Ok(())
}
/// Checkout the `DocState` to a specific version.
///
/// > The document becomes detached during a `checkout` operation.
/// > Being `detached` implies that the `DocState` is not synchronized with the latest version of the `OpLog`.
/// > In a detached state, the document is not editable, and any `import` operations will be
/// > recorded in the `OpLog` without being applied to the `DocState`.
///
/// You should call `attach` to attach the `DocState` to the latest version of `OpLog`.
///
/// @param frontiers - the specific frontiers
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// const frontiers = doc.frontiers();
/// text.insert(0, "Hello World!");
/// loro.checkout(frontiers);
/// console.log(doc.toJSON()); // {"text": ""}
/// ```
pub fn checkout(&mut self, frontiers: Vec<JsID>) -> JsResult<()> {
self.0.checkout(&ids_to_frontiers(frontiers)?)?;
Ok(())
}
/// Peer ID of the current writer.
#[wasm_bindgen(js_name = "peerId", method, getter)]
pub fn peer_id(&self) -> u64 {
self.0.peer_id()
}
/// Get peer id in decimal string.
#[wasm_bindgen(js_name = "peerIdStr", method, getter)]
pub fn peer_id_str(&self) -> JsStrPeerID {
let v: JsValue = format!("{}", self.0.peer_id()).into();
v.into()
}
/// Set the peer ID of the current writer.
///
/// It must be a number, a BigInt, or a decimal string that can be parsed to a unsigned 64-bit integer.
///
/// Note: use it with caution. You need to make sure there is not chance that two peers
/// have the same peer ID. Otherwise, we cannot ensure the consistency of the document.
#[wasm_bindgen(js_name = "setPeerId", method)]
pub fn set_peer_id(&self, peer_id: JsIntoPeerID) -> JsResult<()> {
let id = js_peer_to_peer(peer_id.into())?;
self.0.set_peer_id(id)?;
Ok(())
}
/// Commit the cumulative auto committed transaction.
///
/// You can specify the `origin` and `timestamp` of the commit.
///
/// NOTE: Timestamps are forced to be in ascending order.
/// If you commit a new change with a timestamp that is less than the existing one,
/// the largest existing timestamp will be used instead.
pub fn commit(&self, origin: Option<String>, timestamp: Option<f64>) {
let mut options = CommitOptions::default();
options.set_origin(origin.as_deref());
options.set_timestamp(timestamp.map(|x| x as i64));
self.0.commit_with(options);
}
/// Get a LoroText by container id.
///
/// The object returned is a new js object each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// ```
#[wasm_bindgen(js_name = "getText")]
pub fn get_text(&self, cid: &JsIntoContainerID) -> JsResult<LoroText> {
let text = self
.0
.get_text(js_value_to_container_id(cid, ContainerType::Text)?);
Ok(LoroText {
handler: text,
doc: Some(self.0.clone()),
delta_cache: None,
})
}
/// Get a LoroMap by container id
///
/// The object returned is a new js object each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// ```
#[wasm_bindgen(js_name = "getMap", skip_typescript)]
pub fn get_map(&self, cid: &JsIntoContainerID) -> JsResult<LoroMap> {
let map = self
.0
.get_map(js_value_to_container_id(cid, ContainerType::Map)?);
Ok(LoroMap {
handler: map,
doc: Some(self.0.clone()),
})
}
/// Get a LoroList by container id
///
/// The object returned is a new js object each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// ```
#[wasm_bindgen(js_name = "getList", skip_typescript)]
pub fn get_list(&self, cid: &JsIntoContainerID) -> JsResult<LoroList> {
let list = self
.0
.get_list(js_value_to_container_id(cid, ContainerType::List)?);
Ok(LoroList {
handler: list,
doc: Some(self.0.clone()),
})
}
/// Get a LoroMovableList by container id
///
/// The object returned is a new js object each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getMovableList("list");
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn getMovableList(&self, cid: &JsIntoContainerID) -> JsResult<LoroMovableList> {
let list = self
.0
.get_movable_list(js_value_to_container_id(cid, ContainerType::MovableList)?);
Ok(LoroMovableList {
handler: list,
doc: Some(self.0.clone()),
})
}
/// Get a LoroCounter by container id
#[wasm_bindgen(js_name = "getCounter")]
pub fn get_counter(&self, cid: &JsIntoContainerID) -> JsResult<LoroCounter> {
let counter = self
.0
.get_counter(js_value_to_container_id(cid, ContainerType::Counter)?);
Ok(LoroCounter {
handler: counter,
doc: Some(self.0.clone()),
})
}
/// Get a LoroTree by container id
///
/// The object returned is a new js object each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// ```
#[wasm_bindgen(js_name = "getTree", skip_typescript)]
pub fn get_tree(&self, cid: &JsIntoContainerID) -> JsResult<LoroTree> {
let tree = self
.0
.get_tree(js_value_to_container_id(cid, ContainerType::Tree)?);
Ok(LoroTree {
handler: tree,
doc: Some(self.0.clone()),
})
}
/// Get the container corresponding to the container id
///
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// let text = doc.getText("text");
/// const textId = text.id;
/// text = doc.getContainerById(textId);
/// ```
#[wasm_bindgen(skip_typescript, js_name = "getContainerById")]
pub fn get_container_by_id(&self, container_id: JsContainerID) -> JsResult<JsValue> {
let container_id: ContainerID = container_id.to_owned().try_into()?;
let ty = container_id.container_type();
Ok(match ty {
ContainerType::Map => {
let map = self.0.get_map(container_id);
LoroMap {
handler: map,
doc: Some(self.0.clone()),
}
.into()
}
ContainerType::List => {
let list = self.0.get_list(container_id);
LoroList {
handler: list,
doc: Some(self.0.clone()),
}
.into()
}
ContainerType::Text => {
let richtext = self.0.get_text(container_id);
LoroText {
handler: richtext,
doc: Some(self.0.clone()),
delta_cache: None,
}
.into()
}
ContainerType::Tree => {
let tree = self.0.get_tree(container_id);
LoroTree {
handler: tree,
doc: Some(self.0.clone()),
}
.into()
}
ContainerType::MovableList => {
let movelist = self.0.get_movable_list(container_id);
LoroMovableList {
handler: movelist,
doc: Some(self.0.clone()),
}
.into()
}
ContainerType::Counter => {
let counter = self.0.get_counter(container_id);
LoroCounter {
handler: counter,
doc: Some(self.0.clone()),
}
.into()
}
ContainerType::Unknown(_) => {
return Err(JsValue::from_str(
"You are attempting to get an unknown container",
));
}
})
}
/// Get the encoded version vector of the current document.
///
/// If you checkout to a specific version, the version vector will change.
#[inline(always)]
pub fn version(&self) -> VersionVector {
VersionVector(self.0.state_vv())
}
/// Get the encoded version vector of the latest version in OpLog.
///
/// If you checkout to a specific version, the version vector will not change.
#[wasm_bindgen(js_name = "oplogVersion")]
pub fn oplog_version(&self) -> VersionVector {
VersionVector(self.0.oplog_vv())
}
/// Get the frontiers of the current document.
///
/// If you checkout to a specific version, this value will change.
#[inline]
pub fn frontiers(&self) -> JsIDs {
frontiers_to_ids(&self.0.state_frontiers())
}
/// Get the frontiers of the latest version in OpLog.
///
/// If you checkout to a specific version, this value will not change.
#[inline(always)]
#[wasm_bindgen(js_name = "oplogFrontiers")]
pub fn oplog_frontiers(&self) -> JsIDs {
frontiers_to_ids(&self.0.oplog_frontiers())
}
/// Compare the version of the OpLog with the specified frontiers.
///
/// This method is useful to compare the version by only a small amount of data.
///
/// This method returns an integer indicating the relationship between the version of the OpLog (referred to as 'self')
/// and the provided 'frontiers' parameter:
///
/// - -1: The version of 'self' is either less than 'frontiers' or is non-comparable (parallel) to 'frontiers',
/// indicating that it is not definitively less than 'frontiers'.
/// - 0: The version of 'self' is equal to 'frontiers'.
/// - 1: The version of 'self' is greater than 'frontiers'.
///
/// # Internal
///
/// Frontiers cannot be compared without the history of the OpLog.
///
#[wasm_bindgen(js_name = "cmpWithFrontiers")]
pub fn cmp_with_frontiers(&self, frontiers: Vec<JsID>) -> JsResult<i32> {
let frontiers = ids_to_frontiers(frontiers)?;
Ok(match self.0.cmp_with_frontiers(&frontiers) {
Ordering::Less => -1,
Ordering::Greater => 1,
Ordering::Equal => 0,
})
}
/// Compare the ordering of two Frontiers.
///
/// It's assumed that both Frontiers are included by the doc. Otherwise, an error will be thrown.
///
/// Return value:
///
/// - -1: a < b
/// - 0: a == b
/// - 1: a > b
/// - undefined: a ∥ b: a and b are concurrent
#[wasm_bindgen(js_name = "cmpFrontiers")]
pub fn cmp_frontiers(&self, a: Vec<JsID>, b: Vec<JsID>) -> JsResult<JsPartialOrd> {
let a = ids_to_frontiers(a)?;
let b = ids_to_frontiers(b)?;
let c = self
.0
.cmp_frontiers(&a, &b)
.map_err(|e| JsError::new(&e.to_string()))?;
if let Some(c) = c {
let v: JsValue = match c {
Ordering::Less => -1,
Ordering::Greater => 1,
Ordering::Equal => 0,
}
.into();
Ok(v.into())
} else {
Ok(JsValue::UNDEFINED.into())
}
}
/// Export the snapshot of current version, it's include all content of
/// operations and states
#[wasm_bindgen(js_name = "exportSnapshot")]
pub fn export_snapshot(&self) -> JsResult<Vec<u8>> {
Ok(self.0.export_snapshot())
}
/// Export updates from the specific version to the current version
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// // get all updates of the doc
/// const updates = doc.exportFrom();
/// const version = doc.oplogVersion();
/// text.insert(5, " World");
/// // get updates from specific version to the latest version
/// const updates2 = doc.exportFrom(version);
/// ```
#[wasm_bindgen(skip_typescript, js_name = "exportFrom")]
pub fn export_from(&self, vv: Option<VersionVector>) -> JsResult<Vec<u8>> {
if let Some(vv) = vv {
// `version` may be null or undefined
Ok(self.0.export_from(&vv.0))
} else {
Ok(self.0.export_from(&Default::default()))
}
}
/// Export updates from the specific version to the current version with JSON format.
#[wasm_bindgen(js_name = "exportJsonUpdates")]
pub fn export_json_updates(
&self,
start_vv: Option<VersionVector>,
end_vv: Option<VersionVector>,
) -> JsResult<JsJsonSchema> {
let mut json_start_vv = Default::default();
if let Some(vv) = start_vv {
json_start_vv = vv.0;
}
let mut json_end_vv = self.oplog_version().0;
if let Some(vv) = end_vv {
json_end_vv = vv.0;
}
let json_schema = self.0.export_json_updates(&json_start_vv, &json_end_vv);
let s = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
let v = json_schema
.serialize(&s)
.map_err(std::convert::Into::<JsValue>::into)?;
Ok(v.into())
}
/// Import updates from the JSON format.
///
/// only supports backward compatibility but not forward compatibility.
#[wasm_bindgen(js_name = "importJsonUpdates")]
pub fn import_json_updates(&self, json: JsJsonSchemaOrString) -> JsResult<()> {
let json: JsValue = json.into();
if JsValue::is_string(&json) {
let json_str = json.as_string().unwrap();
return self
.0
.import_json_updates(json_str.as_str())
.map_err(|e| e.into());
}
let json_schema: JsonSchema = serde_wasm_bindgen::from_value(json)?;
self.0.import_json_updates(json_schema)?;
Ok(())
}
/// Import a snapshot or a update to current doc.
///
/// Note:
/// - Updates within the current version will be ignored
/// - Updates with missing dependencies will be pending until the dependencies are received
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// // get all updates of the doc
/// const updates = doc.exportFrom();
/// const snapshot = doc.exportSnapshot();
/// const doc2 = new Loro();
/// // import snapshot
/// doc2.import(snapshot);
/// // or import updates
/// doc2.import(updates);
/// ```
pub fn import(&self, update_or_snapshot: &[u8]) -> JsResult<()> {
self.0.import(update_or_snapshot)?;
Ok(())
}
/// Import a batch of updates.
///
/// It's more efficient than importing updates one by one.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// const updates = doc.exportFrom();
/// const snapshot = doc.exportSnapshot();
/// const doc2 = new Loro();
/// doc2.importUpdateBatch([snapshot, updates]);
/// ```
#[wasm_bindgen(js_name = "importUpdateBatch")]
pub fn import_update_batch(&mut self, data: Array) -> JsResult<()> {
let data = data
.iter()
.map(|x| {
let arr: Uint8Array = Uint8Array::new(&x);
arr.to_vec()
})
.collect::<Vec<_>>();
if data.is_empty() {
return Ok(());
}
Ok(self.0.import_batch(&data)?)
}
/// Get the json format of the document state.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, "Hello");
/// const text = list.insertContainer(0, new LoroText());
/// text.insert(0, "Hello");
/// const map = list.insertContainer(1, new LoroMap());
/// map.set("foo", "bar");
/// /*
/// {"list": ["Hello", {"foo": "bar"}]}
/// *\/
/// console.log(doc.toJSON());
/// ```
#[wasm_bindgen(js_name = "toJSON")]
pub fn to_json(&self) -> JsResult<JsValue> {
let json = self.0.get_deep_value();
Ok(json.into())
}
/// Subscribe to the changes of the loro document. The function will be called when the
/// transaction is committed or updates from remote are imported.
///
/// Returns a subscription ID, which can be used to unsubscribe.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// doc.subscribe((event)=>{
/// console.log(event);
/// });
/// text.insert(0, "Hello");
/// // the events will be emitted when `commit()` is called.
/// doc.commit();
/// ```
// TODO: convert event and event sub config
pub fn subscribe(&self, f: js_sys::Function) -> u32 {
let observer = observer::Observer::new(f);
let doc = self.0.clone();
self.0
.subscribe_root(Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc)
// call_subscriber(observer.clone(), e);
}))
.into_u32()
}
/// Unsubscribe by the subscription id.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// const subscription = doc.subscribe((event)=>{
/// console.log(event);
/// });
/// text.insert(0, "Hello");
/// // the events will be emitted when `commit()` is called.
/// doc.commit();
/// doc.unsubscribe(subscription);
/// ```
pub fn unsubscribe(&self, subscription: u32) {
self.0.unsubscribe(SubID::from_u32(subscription))
}
/// Debug the size of the history
#[wasm_bindgen(js_name = "debugHistory")]
pub fn debug_history(&self) {
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().lock().unwrap();
console_log!("{:#?}", oplog.diagnose_size());
}
/// Get all of changes in the oplog
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// const changes = doc.getAllChanges();
///
/// for (let [peer, changes] of changes.entries()){
/// console.log("peer: ", peer);
/// for (let change in changes){
/// console.log("change: ", change);
/// }
/// }
/// ```
#[wasm_bindgen(js_name = "getAllChanges")]
pub fn get_all_changes(&self) -> JsChanges {
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().lock().unwrap();
let mut changes: FxHashMap<PeerID, Vec<ChangeMeta>> = FxHashMap::default();
oplog.change_store().visit_all_changes(&mut |c| {
let change_meta = ChangeMeta {
lamport: c.lamport(),
length: c.atom_len() as u32,
peer: c.peer().to_string(),
counter: c.id().counter,
deps: c
.deps()
.iter()
.map(|dep| StringID {
peer: dep.peer.to_string(),
counter: dep.counter,
})
.collect(),
timestamp: c.timestamp() as f64,
};
changes.entry(c.peer()).or_default().push(change_meta);
});
let ans = js_sys::Map::new();
for (peer_id, changes) in changes {
let row = js_sys::Array::new_with_length(changes.len() as u32);
for (i, change) in changes.iter().enumerate() {
row.set(i as u32, change.to_js());
}
ans.set(&peer_id.to_string().into(), &row);
}
let value: JsValue = ans.into();
value.into()
}
/// Get the change of a specific ID
#[wasm_bindgen(js_name = "getChangeAt")]
pub fn get_change_at(&self, id: JsID) -> JsResult<JsChange> {
let id = js_id_to_id(id)?;
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().lock().unwrap();
let change = oplog
.get_change_at(id)
.ok_or_else(|| JsError::new(&format!("Change {:?} not found", id)))?;
let change = ChangeMeta {
lamport: change.lamport(),
length: change.atom_len() as u32,
peer: change.peer().to_string(),
counter: change.id().counter,
deps: change
.deps()
.iter()
.map(|dep| StringID {
peer: dep.peer.to_string(),
counter: dep.counter,
})
.collect(),
timestamp: change.timestamp() as f64,
};
Ok(change.to_js().into())
}
/// Get the change of with specific peer_id and lamport <= given lamport
#[wasm_bindgen(js_name = "getChangeAtLamport")]
pub fn get_change_at_lamport(
&self,
peer_id: &str,
lamport: u32,
) -> JsResult<JsChangeOrUndefined> {
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().lock().unwrap();
let Some(change) =
oplog.get_change_with_lamport_lte(peer_id.parse().unwrap_throw(), lamport)
else {
return Ok(JsValue::UNDEFINED.into());
};
let change = ChangeMeta {
lamport: change.lamport(),
length: change.atom_len() as u32,
peer: change.peer().to_string(),
counter: change.id().counter,
deps: change
.deps()
.iter()
.map(|dep| StringID {
peer: dep.peer.to_string(),
counter: dep.counter,
})
.collect(),
timestamp: change.timestamp() as f64,
};
Ok(change.to_js().into())
}
/// Get all ops of the change of a specific ID
#[wasm_bindgen(js_name = "getOpsInChange")]
pub fn get_ops_in_change(&self, id: JsID) -> JsResult<Vec<JsValue>> {
let id = js_id_to_id(id)?;
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().lock().unwrap();
let change = oplog
.get_remote_change_at(id)
.ok_or_else(|| JsError::new(&format!("Change {:?} not found", id)))?;
let ops = change
.ops()
.iter()
.map(|op| serde_wasm_bindgen::to_value(op).unwrap())
.collect::<Vec<_>>();
Ok(ops)
}
/// Convert frontiers to a readable version vector
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// const frontiers = doc.frontiers();
/// const version = doc.frontiersToVV(frontiers);
/// ```
#[wasm_bindgen(js_name = "frontiersToVV")]
pub fn frontiers_to_vv(&self, frontiers: Vec<JsID>) -> JsResult<VersionVector> {
let frontiers = ids_to_frontiers(frontiers)?;
let borrow_mut = &self.0;
let oplog = borrow_mut.oplog().try_lock().unwrap();
oplog
.dag()
.frontiers_to_vv(&frontiers)
.map(VersionVector)
.ok_or_else(|| JsError::new("Frontiers not found").into())
}
/// Convert a version vector to frontiers
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// const version = doc.version();
/// const frontiers = doc.vvToFrontiers(version);
/// ```
#[wasm_bindgen(js_name = "vvToFrontiers")]
pub fn vv_to_frontiers(&self, vv: &VersionVector) -> JsResult<JsIDs> {
let f = self.0.oplog().lock().unwrap().dag().vv_to_frontiers(&vv.0);
Ok(frontiers_to_ids(&f))
}
/// Get the value or container at the given path
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("key", 1);
/// console.log(doc.getByPath("map/key")); // 1
/// console.log(doc.getByPath("map")); // LoroMap
/// ```
#[wasm_bindgen(js_name = "getByPath")]
pub fn get_by_path(&self, path: &str) -> JsValueOrContainerOrUndefined {
let ans = self.0.get_by_str_path(path);
let v: JsValue = match ans {
Some(ValueOrHandler::Handler(h)) => handler_to_js_value(h, Some(self.0.clone())),
Some(ValueOrHandler::Value(v)) => v.into(),
None => JsValue::UNDEFINED,
};
v.into()
}
/// Get the absolute position of the given Cursor
///
/// @example
/// ```ts
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "123");
/// const pos0 = text.getCursor(0, 0);
/// {
/// const ans = doc.getCursorPos(pos0!);
/// expect(ans.offset).toBe(0);
/// }
/// text.insert(0, "1");
/// {
/// const ans = doc.getCursorPos(pos0!);
/// expect(ans.offset).toBe(1);
/// }
/// ```
pub fn getCursorPos(&self, cursor: &Cursor) -> JsResult<JsCursorQueryAns> {
let ans = self
.0
.query_pos(&cursor.pos)
.map_err(|e| JsError::new(&e.to_string()))?;
let obj = Object::new();
let update = ans.update.map(|u| Cursor { pos: u });
if let Some(update) = update {
let update_value: JsValue = update.into();
Reflect::set(&obj, &JsValue::from_str("update"), &update_value)?;
}
Reflect::set(
&obj,
&JsValue::from_str("offset"),
&JsValue::from(ans.current.pos),
)?;
Reflect::set(
&obj,
&JsValue::from_str("side"),
&JsValue::from(ans.current.side.to_i32()),
)?;
Ok(JsValue::from(obj).into())
}
}
#[allow(unused)]
fn call_subscriber(ob: observer::Observer, e: DiffEvent, doc: &Arc<LoroDoc>) {
// We convert the event to js object here, so that we don't need to worry about GC.
// In the future, when FinalizationRegistry[1] is stable, we can use `--weak-ref`[2] feature
// in wasm-bindgen to avoid this.
//
// [1]: https://caniuse.com/?search=FinalizationRegistry
// [2]: https://rustwasm.github.io/wasm-bindgen/reference/weak-references.html
let event = diff_event_to_js_value(e, doc);
if let Err(e) = ob.call1(&event) {
throw_error_after_micro_task(e);
}
}
#[allow(unused)]
fn call_after_micro_task(ob: observer::Observer, event: DiffEvent, doc: &Arc<LoroDoc>) {
let promise = Promise::resolve(&JsValue::NULL);
type C = Closure<dyn FnMut(JsValue)>;
let drop_handler: Rc<RefCell<Option<C>>> = Rc::new(RefCell::new(None));
let copy = drop_handler.clone();
let event = diff_event_to_js_value(event, doc);
let closure = Closure::once(move |_: JsValue| {
let ans = ob.call1(&event);
drop(copy);
if let Err(e) = ans {
throw_error_after_micro_task(e)
}
});
let _ = promise.then(&closure);
drop_handler.borrow_mut().replace(closure);
}
impl Default for Loro {
fn default() -> Self {
Self::new()
}
}
fn diff_event_to_js_value(event: DiffEvent, doc: &Arc<LoroDoc>) -> JsValue {
let obj = js_sys::Object::new();
Reflect::set(&obj, &"by".into(), &event.event_meta.by.to_string().into()).unwrap();
let origin: &str = &event.event_meta.origin;
Reflect::set(&obj, &"origin".into(), &JsValue::from_str(origin)).unwrap();
if let Some(t) = event.current_target.as_ref() {
Reflect::set(&obj, &"currentTarget".into(), &t.to_string().into()).unwrap();
}
let events = js_sys::Array::new_with_length(event.events.len() as u32);
for (i, &event) in event.events.iter().enumerate() {
events.set(i as u32, container_diff_to_js_value(event, doc));
}
Reflect::set(&obj, &"events".into(), &events.into()).unwrap();
obj.into()
}
/// /**
/// * The concrete event of Loro.
/// */
/// export interface LoroEvent {
/// /**
/// * The container ID of the event's target.
/// */
/// target: ContainerID;
/// diff: Diff;
/// /**
/// * The absolute path of the event's emitter, which can be an index of a list container or a key of a map container.
/// */
/// path: Path;
/// }
///
fn container_diff_to_js_value(event: &loro_internal::ContainerDiff, doc: &Arc<LoroDoc>) -> JsValue {
let obj = js_sys::Object::new();
Reflect::set(&obj, &"target".into(), &event.id.to_string().into()).unwrap();
Reflect::set(&obj, &"diff".into(), &resolved_diff_to_js(&event.diff, doc)).unwrap();
Reflect::set(
&obj,
&"path".into(),
&convert_container_path_to_js_value(&event.path),
)
.unwrap();
obj.into()
}
fn convert_container_path_to_js_value(path: &[(ContainerID, Index)]) -> JsValue {
let arr = Array::new_with_length(path.len() as u32);
for (i, p) in path.iter().enumerate() {
arr.set(i as u32, p.1.clone().into());
}
let path: JsValue = arr.into_js_result().unwrap();
path
}
/// The handler of a text container. It supports rich text CRDT.
///
/// ## Updating Text Content Using a Diff Algorithm
///
/// A common requirement is to update the current text to a target text.
/// You can implement this using a text diff algorithm of your choice.
/// Below is a sample you can directly copy into your code, which uses the
/// [fast-diff](https://www.npmjs.com/package/fast-diff) package.
///
/// ```ts
/// import { diff } from "fast-diff";
/// import { LoroText } from "loro-crdt";
///
/// function updateText(text: LoroText, newText: string) {
/// const src = text.toString();
/// const delta = diff(src, newText);
/// let index = 0;
/// for (const [op, text] of delta) {
/// if (op === 0) {
/// index += text.length;
/// } else if (op === 1) {
/// text.insert(index, text);
/// index += text.length;
/// } else {
/// text.delete(index, text.length);
/// }
/// }
/// ```
///
///
/// Learn more at https://loro.dev/docs/tutorial/text
#[derive(Clone)]
#[wasm_bindgen]
pub struct LoroText {
handler: TextHandler,
doc: Option<Arc<LoroDoc>>,
delta_cache: Option<(usize, JsValue)>,
}
#[derive(Serialize, Deserialize)]
struct MarkRange {
start: usize,
end: usize,
}
#[wasm_bindgen]
impl LoroText {
/// Create a new detached LoroText.
///
/// The edits on a detached container will not be persisted.
/// To attach the container to the document, please insert it into an attached container.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handler: TextHandler::new_detached(),
doc: None,
delta_cache: None,
}
}
/// "Text"
pub fn kind(&self) -> JsTextStr {
JsValue::from_str("Text").into()
}
/// Iterate each span(internal storage unit) of the text.
///
/// The callback function will be called for each span in the text.
/// If the callback returns `false`, the iteration will stop.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello")
/// text.iter((str) => (console.log(str), true));
/// ```
pub fn iter(&self, callback: &js_sys::Function) {
let context = JsValue::NULL;
self.handler.iter(|c| {
let result = callback.call1(&context, &JsValue::from(c)).unwrap();
result.as_bool().unwrap()
})
}
/// Insert some string at index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// ```
pub fn insert(&mut self, index: usize, content: &str) -> JsResult<()> {
self.handler.insert(index, content)?;
Ok(())
}
/// Get a string slice.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// text.slice(0, 2); // "He"
/// ```
pub fn slice(&mut self, start_index: usize, end_index: usize) -> JsResult<String> {
match self.handler.slice(start_index, end_index) {
Ok(x) => Ok(x),
Err(x) => Err(x.into()),
}
}
/// Get the character at the given position.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// text.charAt(0); // "H"
/// ```
#[wasm_bindgen(js_name = "charAt")]
pub fn char_at(&mut self, pos: usize) -> JsResult<char> {
match self.handler.char_at(pos) {
Ok(x) => Ok(x),
Err(x) => Err(x.into()),
}
}
/// Delete and return the string at the given range and insert a string at the same position.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// text.splice(2, 3, "llo"); // "llo"
/// ```
pub fn splice(&mut self, pos: usize, len: usize, s: &str) -> JsResult<String> {
match self.handler.splice(pos, len, s) {
Ok(x) => Ok(x),
Err(x) => Err(x.into()),
}
}
/// Insert some string at utf-8 index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insertUtf8(0, "Hello");
/// ```
#[wasm_bindgen(js_name = "insertUtf8")]
pub fn insert_utf8(&mut self, index: usize, content: &str) -> JsResult<()> {
self.handler.insert_utf8(index, content)?;
Ok(())
}
/// Delete elements from index to index + len
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "Hello");
/// text.delete(1, 3);
/// const s = text.toString();
/// console.log(s); // "Ho"
/// ```
pub fn delete(&mut self, index: usize, len: usize) -> JsResult<()> {
self.handler.delete(index, len)?;
Ok(())
}
/// Delete elements from index to utf-8 index + len
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insertUtf8(0, "Hello");
/// text.deleteUtf8(1, 3);
/// const s = text.toString();
/// console.log(s); // "Ho"
/// ```
#[wasm_bindgen(js_name = "deleteUtf8")]
pub fn delete_utf8(&mut self, index: usize, len: usize) -> JsResult<()> {
self.handler.delete_utf8(index, len)?;
Ok(())
}
/// Mark a range of text with a key and a value.
///
/// > You should call `configTextStyle` before using `mark` and `unmark`.
///
/// You can use it to create a highlight, make a range of text bold, or add a link to a range of text.
///
/// Note: this is not suitable for unmergeable annotations like comments.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// doc.configTextStyle({bold: {expand: "after"}});
/// const text = doc.getText("text");
/// text.insert(0, "Hello World!");
/// text.mark({ start: 0, end: 5 }, "bold", true);
/// ```
pub fn mark(&self, range: JsRange, key: &str, value: JsValue) -> Result<(), JsError> {
let range: MarkRange = serde_wasm_bindgen::from_value(range.into())?;
let value: LoroValue = LoroValue::from(value);
self.handler.mark(range.start, range.end, key, value)?;
Ok(())
}
/// Unmark a range of text with a key and a value.
///
/// > You should call `configTextStyle` before using `mark` and `unmark`.
///
/// You can use it to remove highlights, bolds or links
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// doc.configTextStyle({bold: {expand: "after"}});
/// const text = doc.getText("text");
/// text.insert(0, "Hello World!");
/// text.mark({ start: 0, end: 5 }, "bold", true);
/// text.unmark({ start: 0, end: 5 }, "bold");
/// ```
pub fn unmark(&self, range: JsRange, key: &str) -> Result<(), JsValue> {
// Internally, this may be marking with null or deleting all the marks with key in the range entirely.
let range: MarkRange = serde_wasm_bindgen::from_value(range.into())?;
self.handler.unmark(range.start, range.end, key)?;
Ok(())
}
/// Convert the state to string
#[allow(clippy::inherent_to_string)]
#[wasm_bindgen(js_name = "toString")]
pub fn to_string(&self) -> String {
self.handler.get_value().as_string().unwrap().to_string()
}
/// Get the text in [Delta](https://quilljs.com/docs/delta/) format.
///
/// The returned value will include the rich text information.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// doc.configTextStyle({bold: {expand: "after"}});
/// text.insert(0, "Hello World!");
/// text.mark({ start: 0, end: 5 }, "bold", true);
/// console.log(text.toDelta()); // [ { insert: 'Hello', attributes: { bold: true } } ]
/// ```
#[wasm_bindgen(js_name = "toDelta")]
pub fn to_delta(&mut self) -> JsStringDelta {
let version = self.handler.version_id();
if let Some((v, delta)) = self.delta_cache.as_ref() {
if *v == version {
return delta.clone().into();
}
}
let delta = self.handler.get_richtext_value();
let value: JsValue = delta.into();
let ans: JsStringDelta = value.clone().into();
self.delta_cache = Some((version, value));
ans
}
/// Get the container id of the text.
#[wasm_bindgen(js_name = "id", method, getter)]
pub fn id(&self) -> JsContainerID {
let value: JsValue = (&self.handler.id()).into();
value.into()
}
/// Get the length of text
#[wasm_bindgen(js_name = "length", method, getter)]
pub fn length(&self) -> usize {
self.handler.len_utf16()
}
/// Subscribe to the changes of the text.
///
/// returns a subscription id, which can be used to unsubscribe.
pub fn subscribe(&self, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let doc = self
.doc
.clone()
.ok_or_else(|| JsError::new("Document is not attached"))?;
let doc_clone = doc.clone();
let ans = doc.subscribe(
&self.handler.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc_clone);
}),
);
Ok(ans.into_u32())
}
/// Unsubscribe by the subscription id.
pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> {
self.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
/// Change the state of this text by delta.
///
/// If a delta item is `insert`, it should include all the attributes of the inserted text.
/// Loro's rich text CRDT may make the inserted text inherit some styles when you use
/// `insert` method directly. However, when you use `applyDelta` if some attributes are
/// inherited from CRDT but not included in the delta, they will be removed.
///
/// Another special property of `applyDelta` is if you format an attribute for ranges out of
/// the text length, Loro will insert new lines to fill the gap first. It's useful when you
/// build the binding between Loro and rich text editors like Quill, which might assume there
/// is always a newline at the end of the text implicitly.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// doc.configTextStyle({bold: {expand: "after"}});
/// text.insert(0, "Hello World!");
/// text.mark({ start: 0, end: 5 }, "bold", true);
/// const delta = text.toDelta();
/// const text2 = doc.getText("text2");
/// text2.applyDelta(delta);
/// expect(text2.toDelta()).toStrictEqual(delta);
/// ```
#[wasm_bindgen(js_name = "applyDelta")]
pub fn apply_delta(&self, delta: JsDelta) -> JsResult<()> {
let delta: Vec<TextDelta> = serde_wasm_bindgen::from_value(delta.into())?;
self.handler.apply_delta(&delta)?;
Ok(())
}
/// Get the parent container.
///
/// - The parent container of the root tree is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
pub fn parent(&self) -> JsContainerOrUndefined {
if let Some(p) = self.handler.parent() {
handler_to_js_value(p, self.doc.clone()).into()
} else {
JsContainerOrUndefined::from(JsValue::UNDEFINED)
}
}
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
pub fn is_attached(&self) -> bool {
self.handler.is_attached()
}
/// Get the attached container associated with this.
///
/// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`.
#[wasm_bindgen(js_name = "getAttached")]
pub fn get_attached(&self) -> JsLoroTextOrUndefined {
if self.is_attached() {
let value: JsValue = self.clone().into();
return value.into();
}
if let Some(h) = self.handler.get_attached() {
handler_to_js_value(Handler::Text(h), self.doc.clone()).into()
} else {
JsValue::UNDEFINED.into()
}
}
/// get the cursor at the given position.
#[wasm_bindgen(skip_typescript)]
pub fn getCursor(&self, pos: usize, side: JsSide) -> Option<Cursor> {
let mut side_value = Side::Middle;
if side.is_truthy() {
let num = side.as_f64().expect("Side must be -1 | 0 | 1");
side_value = Side::from_i32(num as i32).expect("Side must be -1 | 0 | 1");
}
self.handler
.get_cursor(pos, side_value)
.map(|pos| Cursor { pos })
}
}
impl Default for LoroText {
fn default() -> Self {
Self::new()
}
}
/// The handler of a map container.
///
/// Learn more at https://loro.dev/docs/tutorial/map
#[derive(Clone)]
#[wasm_bindgen]
pub struct LoroMap {
handler: MapHandler,
doc: Option<Arc<LoroDoc>>,
}
#[wasm_bindgen]
impl LoroMap {
/// Create a new detached LoroMap.
///
/// The edits on a detached container will not be persisted.
/// To attach the container to the document, please insert it into an attached container.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handler: MapHandler::new_detached(),
doc: None,
}
}
/// "Map"
pub fn kind(&self) -> JsMapStr {
JsValue::from_str("Map").into()
}
/// Set the key with the value.
///
/// If the value of the key is exist, the old value will be updated.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// map.set("foo", "baz");
/// ```
#[wasm_bindgen(js_name = "set", skip_typescript)]
pub fn insert(&mut self, key: &str, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.insert(key, v)?;
Ok(())
}
/// Remove the key from the map.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// map.delete("foo");
/// ```
pub fn delete(&mut self, key: &str) -> JsResult<()> {
self.handler.delete(key)?;
Ok(())
}
/// Get the value of the key. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// The object/value returned is a new js object/value each time because it need to cross
/// the WASM boundary.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// const bar = map.get("foo");
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn get(&self, key: &str) -> JsValueOrContainerOrUndefined {
let v = self.handler.get_(key);
(match v {
Some(ValueOrHandler::Handler(c)) => handler_to_js_value(c, self.doc.clone()),
Some(ValueOrHandler::Value(v)) => v.into(),
None => JsValue::UNDEFINED,
})
.into()
}
/// Get the value of the key. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// The object returned is a new js object each time because it need to cross
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// const bar = map.get("foo");
/// ```
#[wasm_bindgen(js_name = "getOrCreateContainer", skip_typescript)]
pub fn get_or_create_container(&self, key: &str, child: JsContainer) -> JsResult<JsContainer> {
let child = convert::js_to_container(child)?;
let handler = self
.handler
.get_or_create_container(key, child.to_handler())?;
Ok(handler_to_js_value(handler, self.doc.clone()).into())
}
/// Get the keys of the map.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// map.set("baz", "bar");
/// const keys = map.keys(); // ["foo", "baz"]
/// ```
pub fn keys(&self) -> Vec<JsValue> {
let mut ans = Vec::with_capacity(self.handler.len());
self.handler.for_each(|k, _| {
ans.push(k.to_string().into());
});
ans
}
/// Get the values of the map. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// map.set("baz", "bar");
/// const values = map.values(); // ["bar", "bar"]
/// ```
pub fn values(&self) -> Vec<JsValue> {
let mut ans: Vec<JsValue> = Vec::with_capacity(self.handler.len());
self.handler.for_each(|_, v| {
ans.push(loro_value_to_js_value_or_container(v, self.doc.clone()));
});
ans
}
/// Get the entries of the map. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// map.set("baz", "bar");
/// const entries = map.entries(); // [["foo", "bar"], ["baz", "bar"]]
/// ```
pub fn entries(&self) -> Vec<MapEntry> {
let mut ans: Vec<MapEntry> = Vec::with_capacity(self.handler.len());
self.handler.for_each(|k, v| {
let array = Array::new();
array.push(&k.to_string().into());
array.push(&loro_value_to_js_value_or_container(v, self.doc.clone()));
let v: JsValue = array.into();
ans.push(v.into());
});
ans
}
/// The container id of this handler.
#[wasm_bindgen(js_name = "id", method, getter)]
pub fn id(&self) -> JsContainerID {
let value: JsValue = (&self.handler.id()).into();
value.into()
}
/// Get the keys and the values. If the type of value is a child container,
/// it will be resolved recursively.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// const text = map.setContainer("text", new LoroText());
/// text.insert(0, "Hello");
/// console.log(map.getDeepValue()); // {"foo": "bar", "text": "Hello"}
/// ```
#[wasm_bindgen(js_name = "toJSON")]
pub fn to_json(&self) -> JsValue {
self.handler.get_deep_value().into()
}
/// Set the key with a container.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// const text = map.setContainer("text", new LoroText());
/// const list = map.setContainer("list", new LoroText());
/// ```
#[wasm_bindgen(js_name = "setContainer", skip_typescript)]
pub fn insert_container(&mut self, key: &str, child: JsContainer) -> JsResult<JsContainer> {
let child = convert::js_to_container(child)?;
let c = self.handler.insert_container(key, child.to_handler())?;
Ok(handler_to_js_value(c, self.doc.clone()).into())
}
/// Subscribe to the changes of the map.
///
/// returns a subscription id, which can be used to unsubscribe.
///
/// @param {Listener} f - Event listener
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.subscribe((event)=>{
/// console.log(event);
/// });
/// map.set("foo", "bar");
/// doc.commit();
/// ```
pub fn subscribe(&self, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let doc = self
.doc
.clone()
.ok_or_else(|| JsError::new("Document is not attached"))?;
let doc_clone = doc.clone();
let id = doc.subscribe(
&self.handler.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc_clone);
}),
);
Ok(id.into_u32())
}
/// Unsubscribe by the subscription.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// const subscription = map.subscribe((event)=>{
/// console.log(event);
/// });
/// map.set("foo", "bar");
/// doc.commit();
/// map.unsubscribe(subscription);
/// ```
pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> {
self.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
/// Get the size of the map.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const map = doc.getMap("map");
/// map.set("foo", "bar");
/// console.log(map.size); // 1
/// ```
#[wasm_bindgen(js_name = "size", method, getter)]
pub fn size(&self) -> usize {
self.handler.len()
}
/// Get the parent container.
///
/// - The parent container of the root tree is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
pub fn parent(&self) -> JsContainerOrUndefined {
if let Some(p) = self.handler.parent() {
handler_to_js_value(p, self.doc.clone()).into()
} else {
JsContainerOrUndefined::from(JsValue::UNDEFINED)
}
}
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
pub fn is_attached(&self) -> bool {
self.handler.is_attached()
}
/// Get the attached container associated with this.
///
/// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`.
#[wasm_bindgen(js_name = "getAttached")]
pub fn get_attached(&self) -> JsLoroMapOrUndefined {
if self.is_attached() {
let value: JsValue = self.clone().into();
return value.into();
}
let Some(h) = self.handler.get_attached() else {
return JsValue::UNDEFINED.into();
};
handler_to_js_value(Handler::Map(h), self.doc.clone()).into()
}
}
impl Default for LoroMap {
fn default() -> Self {
Self::new()
}
}
/// The handler of a list container.
///
/// Learn more at https://loro.dev/docs/tutorial/list
#[derive(Clone)]
#[wasm_bindgen]
pub struct LoroList {
handler: ListHandler,
doc: Option<Arc<LoroDoc>>,
}
#[wasm_bindgen]
impl LoroList {
/// Create a new detached LoroList.
///
/// The edits on a detached container will not be persisted.
/// To attach the container to the document, please insert it into an attached container.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handler: ListHandler::new_detached(),
doc: None,
}
}
/// "List"
pub fn kind(&self) -> JsListStr {
JsValue::from_str("List").into()
}
/// Insert a value at index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// console.log(list.value); // [100, "foo", true];
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn insert(&mut self, index: usize, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.insert(index, v)?;
Ok(())
}
/// Delete elements from index to index + len.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.delete(0, 1);
/// console.log(list.value); // []
/// ```
pub fn delete(&mut self, index: usize, len: usize) -> JsResult<()> {
self.handler.delete(index, len)?;
Ok(())
}
/// Get the value at the index. If the value is a container, the corresponding handler will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// console.log(list.get(0)); // 100
/// console.log(list.get(1)); // undefined
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn get(&self, index: usize) -> JsValueOrContainerOrUndefined {
let Some(v) = self.handler.get_(index) else {
return JsValue::UNDEFINED.into();
};
(match v {
ValueOrHandler::Value(v) => v.into(),
ValueOrHandler::Handler(h) => handler_to_js_value(h, self.doc.clone()),
})
.into()
}
/// Get the id of this container.
#[wasm_bindgen(js_name = "id", method, getter)]
pub fn id(&self) -> JsContainerID {
let value: JsValue = (&self.handler.id()).into();
value.into()
}
/// Get elements of the list. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// list.insertContainer(3, new LoroText());
/// console.log(list.value); // [100, "foo", true, LoroText];
/// ```
#[wasm_bindgen(js_name = "toArray", method, skip_typescript)]
pub fn to_array(&mut self) -> Vec<JsValueOrContainer> {
let mut arr: Vec<JsValueOrContainer> = Vec::with_capacity(self.length());
self.handler.for_each(|(_, x)| {
arr.push(match x {
ValueOrHandler::Value(v) => {
let v: JsValue = v.into();
v.into()
}
ValueOrHandler::Handler(h) => {
let v: JsValue = handler_to_js_value(h, self.doc.clone());
v.into()
}
});
});
arr
}
/// Get elements of the list. If the type of a element is a container, it will be
/// resolved recursively.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// const text = list.insertContainer(1, new LoroText());
/// text.insert(0, "Hello");
/// console.log(list.getDeepValue()); // [100, "Hello"];
/// ```
#[wasm_bindgen(js_name = "toJSON")]
pub fn to_json(&self) -> JsValue {
let value = self.handler.get_deep_value();
value.into()
}
/// Insert a container at the index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// const text = list.insertContainer(1, new LoroText());
/// text.insert(0, "Hello");
/// console.log(list.getDeepValue()); // [100, "Hello"];
/// ```
#[wasm_bindgen(js_name = "insertContainer", skip_typescript)]
pub fn insert_container(&mut self, index: usize, child: JsContainer) -> JsResult<JsContainer> {
let child = js_to_container(child)?;
let c = self.handler.insert_container(index, child.to_handler())?;
Ok(handler_to_js_value(c, self.doc.clone()).into())
}
/// Subscribe to the changes of the list.
///
/// Returns a subscription id, which can be used to unsubscribe.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.subscribe((event)=>{
/// console.log(event);
/// });
/// list.insert(0, 100);
/// doc.commit();
/// ```
pub fn subscribe(&self, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let doc = self
.doc
.clone()
.ok_or_else(|| JsError::new("Document is not attached"))?;
let doc_clone = doc.clone();
let ans = doc.subscribe(
&self.handler.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc_clone);
}),
);
Ok(ans.into_u32())
}
/// Unsubscribe by the subscription id.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// const subscription = list.subscribe((event)=>{
/// console.log(event);
/// });
/// list.insert(0, 100);
/// doc.commit();
/// list.unsubscribe(subscription);
/// ```
pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> {
self.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
/// Get the length of list.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// console.log(list.length); // 3
/// ```
#[wasm_bindgen(js_name = "length", method, getter)]
pub fn length(&self) -> usize {
self.handler.len()
}
/// Get the parent container.
///
/// - The parent container of the root tree is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
pub fn parent(&self) -> JsContainerOrUndefined {
if let Some(p) = self.handler.parent() {
handler_to_js_value(p, self.doc.clone()).into()
} else {
JsContainerOrUndefined::from(JsValue::UNDEFINED)
}
}
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
pub fn is_attached(&self) -> bool {
self.handler.is_attached()
}
/// Get the attached container associated with this.
///
/// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`.
#[wasm_bindgen(js_name = "getAttached")]
pub fn get_attached(&self) -> JsLoroListOrUndefined {
if self.is_attached() {
let value: JsValue = self.clone().into();
return value.into();
}
if let Some(h) = self.handler.get_attached() {
handler_to_js_value(Handler::List(h), self.doc.clone()).into()
} else {
JsValue::UNDEFINED.into()
}
}
/// Get the cursor at the position.
#[wasm_bindgen(skip_typescript)]
pub fn getCursor(&self, pos: usize, side: JsSide) -> Option<Cursor> {
let mut side_value = Side::Middle;
if side.is_truthy() {
let num = side.as_f64().expect("Side must be -1 | 0 | 1");
side_value = Side::from_i32(num as i32).expect("Side must be -1 | 0 | 1");
}
self.handler
.get_cursor(pos, side_value)
.map(|pos| Cursor { pos })
}
/// Push a value to the end of the list.
#[wasm_bindgen(skip_typescript)]
pub fn push(&self, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.push(v)?;
Ok(())
}
/// Pop a value from the end of the list.
pub fn pop(&self) -> JsResult<Option<JsLoroValue>> {
let v = self.handler.pop()?;
if let Some(v) = v {
let v: JsValue = v.into();
Ok(Some(v.into()))
} else {
Ok(None)
}
}
}
impl Default for LoroList {
fn default() -> Self {
Self::new()
}
}
/// The handler of a list container.
///
/// Learn more at https://loro.dev/docs/tutorial/list
#[derive(Clone)]
#[wasm_bindgen]
pub struct LoroMovableList {
handler: MovableListHandler,
doc: Option<Arc<LoroDoc>>,
}
impl Default for LoroMovableList {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl LoroMovableList {
/// Create a new detached LoroList.
///
/// The edits on a detached container will not be persisted.
/// To attach the container to the document, please insert it into an attached container.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handler: MovableListHandler::new_detached(),
doc: None,
}
}
/// "MovableList"
pub fn kind(&self) -> JsMovableListStr {
JsValue::from_str("MovableList").into()
}
/// Insert a value at index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// console.log(list.value); // [100, "foo", true];
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn insert(&mut self, index: usize, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.insert(index, v)?;
Ok(())
}
/// Delete elements from index to index + len.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.delete(0, 1);
/// console.log(list.value); // []
/// ```
pub fn delete(&mut self, index: usize, len: usize) -> JsResult<()> {
self.handler.delete(index, len)?;
Ok(())
}
/// Get the value at the index. If the value is a container, the corresponding handler will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// console.log(list.get(0)); // 100
/// console.log(list.get(1)); // undefined
/// ```
#[wasm_bindgen(skip_typescript)]
pub fn get(&self, index: usize) -> JsValueOrContainerOrUndefined {
let Some(v) = self.handler.get_(index) else {
return JsValue::UNDEFINED.into();
};
(match v {
ValueOrHandler::Value(v) => v.into(),
ValueOrHandler::Handler(h) => handler_to_js_value(h, self.doc.clone()),
})
.into()
}
/// Get the id of this container.
#[wasm_bindgen(js_name = "id", method, getter)]
pub fn id(&self) -> JsContainerID {
let value: JsValue = (&self.handler.id()).into();
value.into()
}
/// Get elements of the list. If the value is a child container, the corresponding
/// `Container` will be returned.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// list.insertContainer(3, new LoroText());
/// console.log(list.value); // [100, "foo", true, LoroText];
/// ```
#[wasm_bindgen(js_name = "toArray", method, skip_typescript)]
pub fn to_array(&mut self) -> Vec<JsValueOrContainer> {
let mut arr: Vec<JsValueOrContainer> = Vec::with_capacity(self.length());
self.handler.for_each(|x| {
arr.push(match x {
ValueOrHandler::Value(v) => {
let v: JsValue = v.into();
v.into()
}
ValueOrHandler::Handler(h) => {
let v: JsValue = handler_to_js_value(h, self.doc.clone());
v.into()
}
});
});
arr
}
/// Get elements of the list. If the type of a element is a container, it will be
/// resolved recursively.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// const text = list.insertContainer(1, new LoroText());
/// text.insert(0, "Hello");
/// console.log(list.getDeepValue()); // [100, "Hello"];
/// ```
#[wasm_bindgen(js_name = "toJSON")]
pub fn to_json(&self) -> JsValue {
let value = self.handler.get_deep_value();
value.into()
}
/// Insert a container at the index.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// const text = list.insertContainer(1, new LoroText());
/// text.insert(0, "Hello");
/// console.log(list.getDeepValue()); // [100, "Hello"];
/// ```
#[wasm_bindgen(js_name = "insertContainer", skip_typescript)]
pub fn insert_container(&mut self, index: usize, child: JsContainer) -> JsResult<JsContainer> {
let child = js_to_container(child)?;
let c = self.handler.insert_container(index, child.to_handler())?;
Ok(handler_to_js_value(c, self.doc.clone()).into())
}
/// Subscribe to the changes of the list.
///
/// Returns a subscription id, which can be used to unsubscribe.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.subscribe((event)=>{
/// console.log(event);
/// });
/// list.insert(0, 100);
/// doc.commit();
/// ```
pub fn subscribe(&self, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let loro = self
.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?;
let doc_clone = loro.clone();
let ans = loro.subscribe(
&self.handler.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc_clone);
}),
);
Ok(ans.into_u32())
}
/// Unsubscribe by the subscription id.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// const subscription = list.subscribe((event)=>{
/// console.log(event);
/// });
/// list.insert(0, 100);
/// doc.commit();
/// list.unsubscribe(subscription);
/// ```
pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> {
self.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
/// Get the length of list.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const list = doc.getList("list");
/// list.insert(0, 100);
/// list.insert(1, "foo");
/// list.insert(2, true);
/// console.log(list.length); // 3
/// ```
#[wasm_bindgen(js_name = "length", method, getter)]
pub fn length(&self) -> usize {
self.handler.len()
}
/// Get the parent container.
///
/// - The parent container of the root tree is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
pub fn parent(&self) -> JsContainerOrUndefined {
if let Some(p) = self.handler.parent() {
handler_to_js_value(p, self.doc.clone()).into()
} else {
JsContainerOrUndefined::from(JsValue::UNDEFINED)
}
}
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
pub fn is_attached(&self) -> bool {
self.handler.is_attached()
}
/// Get the attached container associated with this.
///
/// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`.
#[wasm_bindgen(js_name = "getAttached")]
pub fn get_attached(&self) -> JsLoroListOrUndefined {
if self.is_attached() {
let value: JsValue = self.clone().into();
return value.into();
}
if let Some(h) = self.handler.get_attached() {
handler_to_js_value(Handler::MovableList(h), self.doc.clone()).into()
} else {
JsValue::UNDEFINED.into()
}
}
/// Get the cursor of the container.
#[wasm_bindgen(skip_typescript)]
pub fn getCursor(&self, pos: usize, side: JsSide) -> Option<Cursor> {
let mut side_value = Side::Middle;
if side.is_truthy() {
let num = side.as_f64().expect("Side must be -1 | 0 | 1");
side_value = Side::from_i32(num as i32).expect("Side must be -1 | 0 | 1");
}
self.handler
.get_cursor(pos, side_value)
.map(|pos| Cursor { pos })
}
/// Move the element from `from` to `to`.
///
/// The new position of the element will be `to`.
/// Move the element from `from` to `to`.
///
/// The new position of the element will be `to`. This method is optimized to prevent redundant
/// operations that might occur with a naive remove and insert approach. Specifically, it avoids
/// creating surplus values in the list, unlike a delete followed by an insert, which can lead to
/// additional values in cases of concurrent edits. This ensures more efficient and accurate
/// operations in a MovableList.
#[wasm_bindgen(js_name = "move")]
pub fn mov(&self, from: usize, to: usize) -> JsResult<()> {
self.handler.mov(from, to)?;
Ok(())
}
/// Set the value at the given position.
///
/// It's different from `delete` + `insert` that it will replace the value at the position.
///
/// For example, if you have a list `[1, 2, 3]`, and you call `set(1, 100)`, the list will be `[1, 100, 3]`.
/// If concurrently someone call `set(1, 200)`, the list will be `[1, 200, 3]` or `[1, 100, 3]`.
///
/// But if you use `delete` + `insert` to simulate the set operation, they may create redundant operations
/// and the final result will be `[1, 100, 200, 3]` or `[1, 200, 100, 3]`.
#[wasm_bindgen(skip_typescript)]
pub fn set(&self, pos: usize, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.set(pos, v)?;
Ok(())
}
/// Set the container at the given position.
#[wasm_bindgen(skip_typescript)]
pub fn setContainer(&self, pos: usize, child: JsContainer) -> JsResult<JsContainer> {
let child = js_to_container(child)?;
let c = self.handler.set_container(pos, child.to_handler())?;
Ok(handler_to_js_value(c, self.doc.clone()).into())
}
/// Push a value to the end of the list.
#[wasm_bindgen(skip_typescript)]
pub fn push(&self, value: JsLoroValue) -> JsResult<()> {
let v: JsValue = value.into();
self.handler.push(v.into())?;
Ok(())
}
/// Pop a value from the end of the list.
pub fn pop(&self) -> JsResult<Option<JsLoroValue>> {
let v = self.handler.pop()?;
Ok(v.map(|v| {
let v: JsValue = v.into();
v.into()
}))
}
}
/// The handler of a tree(forest) container.
///
/// Learn more at https://loro.dev/docs/tutorial/tree
#[derive(Clone)]
#[wasm_bindgen]
pub struct LoroTree {
handler: TreeHandler,
doc: Option<Arc<LoroDoc>>,
}
extern crate alloc;
/// The handler of a tree node.
#[derive(TryFromJsValue, Clone)]
#[wasm_bindgen]
pub struct LoroTreeNode {
id: TreeID,
tree: TreeHandler,
doc: Option<Arc<LoroDoc>>,
}
fn parse_js_parent(parent: &JsParentTreeID) -> JsResult<Option<TreeID>> {
let js_value: JsValue = parent.into();
let parent: Option<TreeID> = if js_value.is_undefined() {
None
} else {
Some(TreeID::try_from(js_value)?)
};
Ok(parent)
}
fn parse_js_tree_node(parent: &JsTreeNodeOrUndefined) -> JsResult<Option<LoroTreeNode>> {
let js_value: &JsValue = parent.as_ref();
let parent: Option<LoroTreeNode> = if js_value.is_undefined() {
None
} else {
Some(LoroTreeNode::try_from(js_value)?)
};
Ok(parent)
}
// TODO: avoid converting
fn parse_js_tree_id(target: &JsTreeID) -> JsResult<TreeID> {
let target: JsValue = target.into();
let target = TreeID::try_from(target)?;
Ok(target)
}
#[wasm_bindgen]
impl LoroTreeNode {
fn from_tree(id: TreeID, tree: TreeHandler, doc: Option<Arc<LoroDoc>>) -> Self {
Self { id, tree, doc }
}
/// The TreeID of the node.
#[wasm_bindgen(getter, js_name = "id")]
pub fn id(&self) -> JsTreeID {
let value: JsValue = self.id.into();
value.into()
}
/// Create a new node as the child of the current node and
/// return an instance of `LoroTreeNode`.
///
/// If the index is not provided, the new node will be appended to the end.
///
/// @example
/// ```typescript
/// import { Loro } from "loro-crdt";
///
/// let doc = new Loro();
/// let tree = doc.getTree("tree");
/// let root = tree.createNode();
/// let node = root.createNode();
/// let node2 = root.createNode(0);
/// // root
/// // / \
/// // node2 node
/// ```
#[wasm_bindgen(js_name = "createNode", skip_typescript)]
pub fn create_node(&self, index: Option<usize>) -> JsResult<LoroTreeNode> {
let id = if let Some(index) = index {
self.tree.create_at(Some(self.id), index)?
} else {
self.tree.create(Some(self.id))?
};
let node = LoroTreeNode::from_tree(id, self.tree.clone(), self.doc.clone());
Ok(node)
}
/// Move this tree node to be a child of the parent.
/// If the parent is undefined, this node will be a root node.
///
/// If the index is not provided, the node will be appended to the end.
///
/// It's not allowed that the target is an ancestor of the parent.
///
/// @example
/// ```ts
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createChildNode();
/// const node = root.createChildNode();
/// const node2 = node.createChildNode();
/// node2.moveTo(undefined, 0);
/// // node2 root
/// // |
/// // node
///
/// ```
#[wasm_bindgen(js_name = "move")]
pub fn mov(&self, parent: &JsTreeNodeOrUndefined, index: Option<usize>) -> JsResult<()> {
let parent: Option<LoroTreeNode> = parse_js_tree_node(parent)?;
if let Some(index) = index {
self.tree.move_to(self.id, parent.map(|x| x.id), index)?
} else {
self.tree.mov(self.id, parent.map(|x| x.id))?;
}
Ok(())
}
/// Move the tree node to be after the target node.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = root.createNode();
/// const node2 = root.createNode();
/// node2.moveAfter(node);
/// // root
/// // / \
/// // node node2
/// ```
#[wasm_bindgen(js_name = "moveAfter")]
pub fn mov_after(&self, target: &LoroTreeNode) -> JsResult<()> {
self.tree.mov_after(self.id, target.id)?;
Ok(())
}
/// Move the tree node to be before the target node.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = root.createNode();
/// const node2 = root.createNode();
/// node2.moveBefore(node);
/// // root
/// // / \
/// // node2 node
/// ```
#[wasm_bindgen(js_name = "moveBefore")]
pub fn mov_before(&self, target: &LoroTreeNode) -> JsResult<()> {
self.tree.mov_before(self.id, target.id)?;
Ok(())
}
/// Get the index of the node in the parent's children.
#[wasm_bindgen]
pub fn index(&self) -> JsResult<Option<usize>> {
let index = self.tree.get_index_by_tree_id(&self.id);
Ok(index)
}
/// Get the `Fractional Index` of the node.
///
/// Note: the tree container must be attached to the document.
#[wasm_bindgen(js_name = "fractionalIndex")]
pub fn fractional_index(&self) -> JsResult<JsPositionOrUndefined> {
if self.tree.is_attached() {
let pos = self.tree.get_position_by_tree_id(&self.id);
let ans = if let Some(pos) = pos.map(|x| x.to_string()) {
JsValue::from_str(&pos).into()
} else {
JsValue::UNDEFINED.into()
};
Ok(ans)
} else {
Err(JsValue::from_str("Tree is detached"))
}
}
/// Get the associated metadata map container of a tree node.
#[wasm_bindgen(getter, skip_typescript)]
pub fn data(&self) -> JsResult<LoroMap> {
let data = self.tree.get_meta(self.id)?;
let map = LoroMap {
handler: data,
doc: self.doc.clone(),
};
Ok(map)
}
/// Get the parent node of this node.
///
/// - The parent of the root node is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
#[wasm_bindgen]
pub fn parent(&self) -> Option<LoroTreeNode> {
let parent = self.tree.get_node_parent(&self.id).flatten();
parent.map(|p| LoroTreeNode::from_tree(p, self.tree.clone(), self.doc.clone()))
}
/// Get the children of this node.
///
/// The objects returned are new js objects each time because they need to cross
/// the WASM boundary.
#[wasm_bindgen(skip_typescript)]
pub fn children(&self) -> JsValue {
let Some(children) = self.tree.children(Some(self.id)) else {
return JsValue::undefined();
};
let children = children.into_iter().map(|c| {
let node = LoroTreeNode::from_tree(c, self.tree.clone(), self.doc.clone());
JsValue::from(node)
});
Array::from_iter(children).into()
}
}
#[wasm_bindgen]
impl LoroTree {
/// Create a new detached LoroTree.
///
/// The edits on a detached container will not be persisted.
/// To attach the container to the document, please insert it into an attached container.
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
handler: TreeHandler::new_detached(),
doc: None,
}
}
/// "Tree"
pub fn kind(&self) -> JsTreeStr {
JsValue::from_str("Tree").into()
}
/// Create a new tree node as the child of parent and return a `LoroTreeNode` instance.
/// If the parent is undefined, the tree node will be a root node.
///
/// If the index is not provided, the new node will be appended to the end.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = tree.createNode(undefined, 0);
///
/// // undefined
/// // / \
/// // node root
/// ```
#[wasm_bindgen(js_name = "createNode", skip_typescript)]
pub fn create_node(
&mut self,
parent: &JsParentTreeID,
index: Option<usize>,
) -> JsResult<LoroTreeNode> {
let parent: Option<TreeID> = parse_js_parent(parent)?;
let id = if let Some(index) = index {
self.handler.create_at(parent, index)?
} else {
self.handler.create(parent)?
};
let node = LoroTreeNode::from_tree(id, self.handler.clone(), self.doc.clone());
Ok(node)
}
/// Move the target tree node to be a child of the parent.
/// It's not allowed that the target is an ancestor of the parent
/// or the target and the parent are the same node.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = root.createNode();
/// const node2 = node.createNode();
/// tree.move(node2, root);
/// // Error will be thrown if move operation creates a cycle
/// tree.move(root, node);
/// ```
#[wasm_bindgen(js_name = "move")]
pub fn mov(
&mut self,
target: &JsTreeID,
parent: &JsParentTreeID,
index: Option<usize>,
) -> JsResult<()> {
let target = parse_js_tree_id(target)?;
let parent = parse_js_parent(parent)?;
if let Some(index) = index {
self.handler.move_to(target, parent, index)?
} else {
self.handler.mov(target, parent)?
};
Ok(())
}
/// Delete a tree node from the forest.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = root.createNode();
/// tree.delete(node.id);
/// ```
pub fn delete(&mut self, target: &JsTreeID) -> JsResult<()> {
let target = parse_js_tree_id(target)?;
self.handler.delete(target)?;
Ok(())
}
/// Get LoroTreeNode by the TreeID.
#[wasm_bindgen(js_name = "getNodeByID", skip_typescript)]
pub fn get_node_by_id(&self, target: &JsTreeID) -> Option<LoroTreeNode> {
let target: JsValue = target.into();
let target = TreeID::try_from(target).ok()?;
if self.handler.contains(target) {
Some(LoroTreeNode::from_tree(
target,
self.handler.clone(),
self.doc.clone(),
))
} else {
None
}
}
/// Get the id of the container.
#[wasm_bindgen(js_name = "id", getter)]
pub fn id(&self) -> JsContainerID {
let value: JsValue = (&self.handler.id()).into();
value.into()
}
/// Return `true` if the tree contains the TreeID, `false` if the target is deleted or wrong.
#[wasm_bindgen(js_name = "has")]
pub fn contains(&self, target: &JsTreeID) -> bool {
let target: JsValue = target.into();
self.handler.contains(target.try_into().unwrap())
}
/// Get the flat array of the forest.
///
/// Note: the metadata will be not resolved. So if you don't only care about hierarchy
/// but also the metadata, you should use `toJson()`.
///
// TODO: perf
#[wasm_bindgen(js_name = "toArray", skip_typescript)]
pub fn to_array(&mut self) -> JsResult<Array> {
let value = self.handler.get_value().into_list().unwrap();
let ans = Array::new();
for v in value.as_ref() {
let v = v.as_map().unwrap();
let id: JsValue = TreeID::try_from(v["id"].as_string().unwrap().as_str())
.unwrap()
.into();
let id: JsTreeID = id.into();
let parent = if let LoroValue::String(p) = &v["parent"] {
Some(TreeID::try_from(p.as_str())?)
} else {
None
};
let parent: JsParentTreeID = parent
.map(|x| LoroTreeNode::from_tree(x, self.handler.clone(), self.doc.clone()).into())
.unwrap_or(JsValue::undefined())
.into();
let index = *v["index"].as_i64().unwrap() as u32;
let position = v["fractional_index"].as_string().unwrap();
let map: LoroMap = self.get_node_by_id(&id).unwrap().data()?;
let obj = Object::new();
js_sys::Reflect::set(&obj, &"id".into(), &id)?;
js_sys::Reflect::set(&obj, &"parent".into(), &parent)?;
js_sys::Reflect::set(&obj, &"index".into(), &JsValue::from(index))?;
js_sys::Reflect::set(
&obj,
&"fractional_index".into(),
&JsValue::from_str(position),
)?;
js_sys::Reflect::set(&obj, &"meta".into(), &map.into())?;
ans.push(&obj);
}
Ok(ans)
}
/// Get the flat array with metadata of the forest.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// root.data.set("color", "red");
/// // [ { id: '0@F2462C4159C4C8D1', parent: null, meta: { color: 'red' } } ]
/// console.log(tree.toJSON());
/// ```
#[wasm_bindgen(js_name = "toJSON")]
pub fn to_json(&self) -> JsValue {
self.handler.get_deep_value().into()
}
/// Get all tree ids of the forest.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const root = tree.createNode();
/// const node = root.createNode();
/// const node2 = node.createNode();
/// console.log(tree.nodes());
/// ```
pub fn nodes(&mut self) -> Vec<LoroTreeNode> {
self.handler
.nodes()
.into_iter()
.map(|n| LoroTreeNode::from_tree(n, self.handler.clone(), self.doc.clone()))
.collect()
}
/// Get the root nodes of the forest.
pub fn roots(&self) -> Vec<LoroTreeNode> {
self.handler
.roots()
.into_iter()
.map(|n| LoroTreeNode::from_tree(n, self.handler.clone(), self.doc.clone()))
.collect()
}
/// Subscribe to the changes of the tree.
///
/// Returns a subscription id, which can be used to unsubscribe.
///
/// Trees have three types of events: `create`, `delete`, and `move`.
/// - `create`: Creates a new node with its `target` TreeID. If `parent` is undefined,
/// a root node is created; otherwise, a child node of `parent` is created.
/// If the node being created was previously deleted and has archived child nodes,
/// create events for these child nodes will also be received.
/// - `delete`: Deletes the target node. The structure and state of the target node and
/// its child nodes are archived, and delete events for the child nodes will not be received.
/// - `move`: Moves the target node. If `parent` is undefined, the target node becomes a root node;
/// otherwise, it becomes a child node of `parent`.
///
/// If a tree container is subscribed, the event of metadata changes will also be received as a MapDiff.
/// And event's `path` will end with `TreeID`.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// tree.subscribe((event)=>{
/// // event.type: "create" | "delete" | "move"
/// });
/// const root = tree.createNode();
/// const node = root.createNode();
/// doc.commit();
/// ```
pub fn subscribe(&self, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let doc = self
.doc
.clone()
.ok_or_else(|| JsError::new("Document is not attached"))?;
let doc_clone = doc.clone();
let ans = doc.subscribe(
&self.handler.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e, &doc_clone);
}),
);
Ok(ans.into_u32())
}
/// Unsubscribe by the subscription id.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// const tree = doc.getTree("tree");
/// const subscription = tree.subscribe((event)=>{
/// console.log(event);
/// });
/// const root = tree.createNode();
/// const node = root.createNode();
/// doc.commit();
/// tree.unsubscribe(subscription);
/// ```
pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> {
self.doc
.as_ref()
.ok_or_else(|| JsError::new("Document is not attached"))?
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
/// Get the parent container of the tree container.
///
/// - The parent container of the root tree is `undefined`.
/// - The object returned is a new js object each time because it need to cross
/// the WASM boundary.
pub fn parent(&self) -> JsContainerOrUndefined {
if let Some(p) = HandlerTrait::parent(&self.handler) {
handler_to_js_value(p, self.doc.clone()).into()
} else {
JsContainerOrUndefined::from(JsValue::UNDEFINED)
}
}
/// Whether the container is attached to a document.
///
/// If it's detached, the operations on the container will not be persisted.
#[wasm_bindgen(js_name = "isAttached")]
pub fn is_attached(&self) -> bool {
self.handler.is_attached()
}
/// Get the attached container associated with this.
///
/// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`.
#[wasm_bindgen(js_name = "getAttached")]
pub fn get_attached(&self) -> JsLoroTreeOrUndefined {
if self.is_attached() {
let value: JsValue = self.clone().into();
return value.into();
}
if let Some(h) = self.handler.get_attached() {
handler_to_js_value(Handler::Tree(h), self.doc.clone()).into()
} else {
JsValue::UNDEFINED.into()
}
}
}
impl Default for LoroTree {
fn default() -> Self {
Self::new()
}
}
/// Cursor is a stable position representation in the doc.
/// When expressing the position of a cursor, using "index" can be unstable
/// because the cursor's position may change due to other deletions and insertions,
/// requiring updates with each edit. To stably represent a position or range within
/// a list structure, we can utilize the ID of each item/character on List CRDT or
/// Text CRDT for expression.
///
/// Loro optimizes State metadata by not storing the IDs of deleted elements. This
/// approach complicates tracking cursors since they rely on these IDs. The solution
/// recalculates position by replaying relevant history to update cursors
/// accurately. To minimize the performance impact of history replay, the system
/// updates cursor info to reference only the IDs of currently present elements,
/// thereby reducing the need for replay.
///
/// @example
/// ```ts
///
/// const doc = new Loro();
/// const text = doc.getText("text");
/// text.insert(0, "123");
/// const pos0 = text.getCursor(0, 0);
/// {
/// const ans = doc.getCursorPos(pos0!);
/// expect(ans.offset).toBe(0);
/// }
/// text.insert(0, "1");
/// {
/// const ans = doc.getCursorPos(pos0!);
/// expect(ans.offset).toBe(1);
/// }
/// ```
#[derive(Clone)]
#[wasm_bindgen]
pub struct Cursor {
pos: cursor::Cursor,
}
#[wasm_bindgen]
impl Cursor {
/// Get the id of the given container.
pub fn containerId(&self) -> JsContainerID {
let js_value: JsValue = self.pos.container.to_string().into();
JsContainerID::from(js_value)
}
/// Get the ID that represents the position.
///
/// It can be undefined if it's not bind into a specific ID.
pub fn pos(&self) -> Option<JsID> {
match self.pos.id {
Some(id) => {
let value: JsValue = id_to_js(&id);
Some(value.into())
}
None => None,
}
}
/// Get which side of the character/list item the cursor is on.
pub fn side(&self) -> JsSide {
JsValue::from(match self.pos.side {
cursor::Side::Left => -1,
cursor::Side::Middle => 0,
cursor::Side::Right => 1,
})
.into()
}
/// Encode the cursor into a Uint8Array.
pub fn encode(&self) -> Vec<u8> {
self.pos.encode()
}
/// Decode the cursor from a Uint8Array.
pub fn decode(data: &[u8]) -> JsResult<Cursor> {
let pos = cursor::Cursor::decode(data).map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(Cursor { pos })
}
/// "Cursor"
pub fn kind(&self) -> JsValue {
JsValue::from_str("Cursor")
}
}
fn loro_value_to_js_value_or_container(
value: ValueOrHandler,
doc: Option<Arc<LoroDoc>>,
) -> JsValue {
match value {
ValueOrHandler::Value(v) => {
let value: JsValue = v.into();
value
}
ValueOrHandler::Handler(c) => {
let handler: JsValue = handler_to_js_value(c, doc.clone());
handler
}
}
}
/// `UndoManager` is responsible for handling undo and redo operations.
///
/// By default, the maxUndoSteps is set to 100, mergeInterval is set to 1000 ms.
///
/// Each commit made by the current peer is recorded as an undo step in the `UndoManager`.
/// Undo steps can be merged if they occur within a specified merge interval.
///
/// Note that undo operations are local and cannot revert changes made by other peers.
/// To undo changes made by other peers, consider using the time travel feature.
///
/// Once the `peerId` is bound to the `UndoManager` in the document, it cannot be changed.
/// Otherwise, the `UndoManager` may not function correctly.
#[wasm_bindgen]
#[derive(Debug)]
pub struct UndoManager {
undo: InnerUndoManager,
doc: Arc<LoroDoc>,
}
#[wasm_bindgen]
impl UndoManager {
/// `UndoManager` is responsible for handling undo and redo operations.
///
/// PeerID cannot be changed during the lifetime of the UndoManager.
///
/// Note that undo operations are local and cannot revert changes made by other peers.
/// To undo changes made by other peers, consider using the time travel feature.
///
/// Each commit made by the current peer is recorded as an undo step in the `UndoManager`.
/// Undo steps can be merged if they occur within a specified merge interval.
///
/// ## Config
///
/// - `mergeInterval`: Optional. The interval in milliseconds within which undo steps can be merged. Default is 1000 ms.
/// - `maxUndoSteps`: Optional. The maximum number of undo steps to retain. Default is 100.
/// - `excludeOriginPrefixes`: Optional. An array of string prefixes. Events with origins matching these prefixes will be excluded from undo steps.
/// - `onPush`: Optional. A callback function that is called when an undo/redo step is pushed.
/// The function can return a meta data value that will be attached to the given stack item.
/// - `onPop`: Optional. A callback function that is called when an undo/redo step is popped.
/// The function will have a meta data value that was attached to the given stack item when
/// `onPush` was called.
#[wasm_bindgen(constructor)]
pub fn new(doc: &Loro, config: JsUndoConfig) -> Self {
let max_undo_steps = Reflect::get(&config, &JsValue::from_str("maxUndoSteps"))
.unwrap_or(JsValue::from_f64(100.0))
.as_f64()
.unwrap_or(100.0) as usize;
let merge_interval = Reflect::get(&config, &JsValue::from_str("mergeInterval"))
.unwrap_or(JsValue::from_f64(1000.0))
.as_f64()
.unwrap_or(1000.0) as i64;
let exclude_origin_prefixes =
Reflect::get(&config, &JsValue::from_str("excludeOriginPrefixes"))
.ok()
.and_then(|val| val.dyn_into::<js_sys::Array>().ok())
.map(|arr| {
arr.iter()
.filter_map(|val| val.as_string())
.collect::<Vec<String>>()
})
.unwrap_or_default();
let on_push = Reflect::get(&config, &JsValue::from_str("onPush")).ok();
let on_pop = Reflect::get(&config, &JsValue::from_str("onPop")).ok();
let mut undo = InnerUndoManager::new(&doc.0);
undo.set_max_undo_steps(max_undo_steps);
undo.set_merge_interval(merge_interval);
for prefix in exclude_origin_prefixes {
undo.add_exclude_origin_prefix(&prefix);
}
let mut ans = UndoManager {
undo,
doc: doc.0.clone(),
};
if let Some(on_push) = on_push {
ans.setOnPush(on_push);
}
if let Some(on_pop) = on_pop {
ans.setOnPop(on_pop);
}
ans
}
/// Undo the last operation.
pub fn undo(&mut self) -> JsResult<bool> {
let executed = self.undo.undo(&self.doc)?;
Ok(executed)
}
/// Redo the last undone operation.
pub fn redo(&mut self) -> JsResult<bool> {
let executed = self.undo.redo(&self.doc)?;
Ok(executed)
}
/// Can undo the last operation.
pub fn canUndo(&self) -> bool {
self.undo.can_undo()
}
/// Can redo the last operation.
pub fn canRedo(&self) -> bool {
self.undo.can_redo()
}
/// The number of max undo steps.
/// If the number of undo steps exceeds this number, the oldest undo step will be removed.
pub fn setMaxUndoSteps(&mut self, steps: usize) {
self.undo.set_max_undo_steps(steps);
}
/// Set the merge interval (in ms).
/// If the interval is set to 0, the undo steps will not be merged.
/// Otherwise, the undo steps will be merged if the interval between the two steps is less than the given interval.
pub fn setMergeInterval(&mut self, interval: f64) {
self.undo.set_merge_interval(interval as i64);
}
/// If a local event's origin matches the given prefix, it will not be recorded in the
/// undo stack.
pub fn addExcludeOriginPrefix(&mut self, prefix: String) {
self.undo.add_exclude_origin_prefix(&prefix)
}
/// Check if the undo manager is bound to the given document.
pub fn checkBinding(&self, doc: &Loro) -> bool {
Arc::ptr_eq(&self.doc, &doc.0)
}
/// Set the on push event listener.
///
/// Every time an undo step or redo step is pushed, the on push event listener will be called.
#[wasm_bindgen(skip_typescript)]
pub fn setOnPush(&mut self, on_push: JsValue) {
let on_push = on_push.dyn_into::<js_sys::Function>().ok();
if let Some(on_push) = on_push {
let on_push = observer::Observer::new(on_push);
self.undo.set_on_push(Some(Box::new(move |kind, span| {
let is_undo = JsValue::from_bool(matches!(kind, UndoOrRedo::Undo));
let counter_range = js_sys::Object::new();
js_sys::Reflect::set(
&counter_range,
&JsValue::from_str("start"),
&JsValue::from_f64(span.start as f64),
)
.unwrap();
js_sys::Reflect::set(
&counter_range,
&JsValue::from_str("end"),
&JsValue::from_f64(span.end as f64),
)
.unwrap();
let mut undo_item_meta = UndoItemMeta::new();
match on_push.call2(&is_undo, &counter_range) {
Ok(v) => {
if let Ok(obj) = v.dyn_into::<js_sys::Object>() {
if let Ok(value) =
js_sys::Reflect::get(&obj, &JsValue::from_str("value"))
{
let value: LoroValue = value.into();
undo_item_meta.value = value;
}
if let Ok(cursors) =
js_sys::Reflect::get(&obj, &JsValue::from_str("cursors"))
{
let cursors: js_sys::Array = cursors.into();
for cursor in cursors.iter() {
let cursor = js_to_cursor(cursor).unwrap_throw();
undo_item_meta.add_cursor(&cursor.pos);
}
}
}
}
Err(e) => {
throw_error_after_micro_task(e);
}
}
undo_item_meta
})));
} else {
self.undo.set_on_push(None);
}
}
/// Set the on pop event listener.
///
/// Every time an undo step or redo step is popped, the on pop event listener will be called.
#[wasm_bindgen(skip_typescript)]
pub fn setOnPop(&mut self, on_pop: JsValue) {
let on_pop = on_pop.dyn_into::<js_sys::Function>().ok();
if let Some(on_pop) = on_pop {
let on_pop = observer::Observer::new(on_pop);
self.undo
.set_on_pop(Some(Box::new(move |kind, span, value| {
let is_undo = JsValue::from_bool(matches!(kind, UndoOrRedo::Undo));
let meta = js_sys::Object::new();
js_sys::Reflect::set(&meta, &JsValue::from_str("value"), &value.value.into())
.unwrap();
let cursors_array = js_sys::Array::new();
for cursor in value.cursors {
let c = Cursor { pos: cursor.cursor };
cursors_array.push(&c.into());
}
js_sys::Reflect::set(&meta, &JsValue::from_str("cursors"), &cursors_array)
.unwrap();
let counter_range = js_sys::Object::new();
js_sys::Reflect::set(
&counter_range,
&JsValue::from_str("start"),
&JsValue::from_f64(span.start as f64),
)
.unwrap();
js_sys::Reflect::set(
&counter_range,
&JsValue::from_str("end"),
&JsValue::from_f64(span.end as f64),
)
.unwrap();
match on_pop.call3(&is_undo, &meta.into(), &counter_range) {
Ok(_) => {}
Err(e) => {
throw_error_after_micro_task(e);
}
}
})));
} else {
self.undo.set_on_pop(None);
}
}
}
/// Use this function to throw an error after the micro task.
///
/// We should avoid panic or use js_throw directly inside a event listener as it might
/// break the internal invariants.
fn throw_error_after_micro_task(error: JsValue) {
let drop_handler = Rc::new(RefCell::new(None));
let drop_handler_clone = drop_handler.clone();
let closure = Closure::once(Box::new(move |_| {
drop(drop_handler_clone);
throw_val(error);
}));
let promise = Promise::resolve(&JsValue::NULL);
let _ = promise.then(&closure);
drop_handler.borrow_mut().replace(closure);
}
/// [VersionVector](https://en.wikipedia.org/wiki/Version_vector)
/// is a map from [PeerID] to [Counter]. Its a right-open interval.
///
/// i.e. a [VersionVector] of `{A: 1, B: 2}` means that A has 1 atomic op and B has 2 atomic ops,
/// thus ID of `{client: A, counter: 1}` is out of the range.
#[wasm_bindgen]
#[derive(Debug, Default)]
pub struct VersionVector(pub(crate) InternalVersionVector);
#[wasm_bindgen]
impl VersionVector {
/// Create a new version vector.
#[wasm_bindgen(constructor)]
pub fn new(value: JsIntoVersionVector) -> JsResult<VersionVector> {
let value: JsValue = value.into();
if value.is_null() || value.is_undefined() {
return Ok(Self::default());
}
let is_bytes = value.is_instance_of::<js_sys::Uint8Array>();
if is_bytes {
let bytes = js_sys::Uint8Array::from(value.clone());
let bytes = bytes.to_vec();
return VersionVector::decode(&bytes);
}
VersionVector::from_json(JsVersionVectorMap::from(value))
}
/// Create a new version vector from a Map.
#[wasm_bindgen(js_name = "parseJSON", method)]
pub fn from_json(version: JsVersionVectorMap) -> JsResult<VersionVector> {
let map: JsValue = version.into();
let map: js_sys::Map = map.into();
let mut vv = InternalVersionVector::new();
for pair in map.entries() {
let pair = pair.unwrap_throw();
let key = Reflect::get(&pair, &0.into()).unwrap_throw();
let peer_id = key.as_string().expect_throw("PeerID must be string");
let value = Reflect::get(&pair, &1.into()).unwrap_throw();
let counter = value.as_f64().expect_throw("Invalid counter") as Counter;
vv.insert(
peer_id
.parse()
.expect_throw(&format!("{} cannot be parsed as u64", peer_id)),
counter,
);
}
Ok(Self(vv))
}
/// Convert the version vector to a Map
#[wasm_bindgen(js_name = "toJSON", method)]
pub fn to_json(&self) -> JsVersionVectorMap {
let vv = &self.0;
let map = js_sys::Map::new();
for (k, v) in vv.iter() {
let k = k.to_string().into();
let v = JsValue::from(*v);
map.set(&k, &v);
}
let value: JsValue = map.into();
JsVersionVectorMap::from(value)
}
/// Encode the version vector into a Uint8Array.
pub fn encode(&self) -> Vec<u8> {
self.0.encode()
}
/// Decode the version vector from a Uint8Array.
pub fn decode(bytes: &[u8]) -> JsResult<VersionVector> {
let vv = InternalVersionVector::decode(bytes)?;
Ok(Self(vv))
}
/// Get the counter of a peer.
pub fn get(&self, peer_id: JsIntoPeerID) -> JsResult<Option<Counter>> {
let id = js_peer_to_peer(peer_id.into())?;
Ok(self.0.get(&id).copied())
}
/// Compare the version vector with another version vector.
///
/// If they are concurrent, return undefined.
pub fn compare(&self, other: &VersionVector) -> Option<i32> {
self.0.partial_cmp(&other.0).map(|o| match o {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
})
}
}
const ID_CONVERT_ERROR: &str = "Invalid peer id. It must be a number, a BigInt, or a decimal string that can be parsed to a unsigned 64-bit integer";
fn js_peer_to_peer(value: JsValue) -> JsResult<u64> {
if value.is_bigint() {
let bigint = js_sys::BigInt::from(value);
let v: u64 = bigint
.try_into()
.map_err(|_| JsValue::from_str(ID_CONVERT_ERROR))?;
Ok(v)
} else if value.is_string() {
let v: u64 = value
.as_string()
.unwrap()
.parse()
.expect_throw(ID_CONVERT_ERROR);
Ok(v)
} else if let Some(v) = value.as_f64() {
Ok(v as u64)
} else {
Err(JsValue::from_str(ID_CONVERT_ERROR))
}
}
enum Container {
Text(LoroText),
Map(LoroMap),
List(LoroList),
Tree(LoroTree),
MovableList(LoroMovableList),
}
impl Container {
fn to_handler(&self) -> Handler {
match self {
Container::Text(t) => Handler::Text(t.handler.clone()),
Container::Map(m) => Handler::Map(m.handler.clone()),
Container::List(l) => Handler::List(l.handler.clone()),
Container::Tree(t) => Handler::Tree(t.handler.clone()),
Container::MovableList(l) => Handler::MovableList(l.handler.clone()),
}
}
}
/// Decode the metadata of the import blob.
///
/// This method is useful to get the following metadata of the import blob:
///
/// - startVersionVector
/// - endVersionVector
/// - startTimestamp
/// - endTimestamp
/// - isSnapshot
/// - changeNum
#[wasm_bindgen(js_name = "decodeImportBlobMeta")]
pub fn decode_import_blob_meta(blob: &[u8]) -> JsResult<JsImportBlobMetadata> {
let meta: ImportBlobMetadata = LoroDoc::decode_import_blob_meta(blob)?;
Ok(meta.into())
}
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
/**
* Container types supported by loro.
*
* It is most commonly used to specify the type of sub-container to be created.
* @example
* ```ts
* import { Loro } from "loro-crdt";
*
* const doc = new Loro();
* const list = doc.getList("list");
* list.insert(0, 100);
* const containerType = "Text";
* const text = list.insertContainer(1, containerType);
* ```
*/
export type ContainerType = "Text" | "Map" | "List"| "Tree" | "MovableList";
export type PeerID = `${number}`;
/**
* The unique id of each container.
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
*
* const doc = new Loro();
* const list = doc.getList("list");
* const containerId = list.id;
* ```
*/
export type ContainerID =
| `cid:root-${string}:${ContainerType}`
| `cid:${number}@${PeerID}:${ContainerType}`;
/**
* The unique id of each tree node.
*/
export type TreeID = `${number}@${PeerID}`;
interface Loro {
/**
* Export updates from the specific version to the current version
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
*
* const doc = new Loro();
* const text = doc.getText("text");
* text.insert(0, "Hello");
* // get all updates of the doc
* const updates = doc.exportFrom();
* const version = doc.oplogVersion();
* text.insert(5, " World");
* // get updates from specific version to the latest version
* const updates2 = doc.exportFrom(version);
* ```
*/
exportFrom(version?: VersionVector): Uint8Array;
/**
*
* Get the container corresponding to the container id
*
*
* @example
* ```ts
* import { Loro } from "loro-crdt";
*
* const doc = new Loro();
* let text = doc.getText("text");
* const textId = text.id;
* text = doc.getContainerById(textId);
* ```
*/
getContainerById(id: ContainerID): Container;
}
/**
* Represents a `Delta` type which is a union of different operations that can be performed.
*
* @typeparam T - The data type for the `insert` operation.
*
* The `Delta` type can be one of three distinct shapes:
*
* 1. Insert Operation:
* - `insert`: The item to be inserted, of type T.
* - `attributes`: (Optional) A dictionary of attributes, describing styles in richtext
*
* 2. Delete Operation:
* - `delete`: The number of elements to delete.
*
* 3. Retain Operation:
* - `retain`: The number of elements to retain.
* - `attributes`: (Optional) A dictionary of attributes, describing styles in richtext
*/
export type Delta<T> =
| {
insert: T;
attributes?: { [key in string]: {} };
retain?: undefined;
delete?: undefined;
}
| {
delete: number;
attributes?: undefined;
retain?: undefined;
insert?: undefined;
}
| {
retain: number;
attributes?: { [key in string]: {} };
delete?: undefined;
insert?: undefined;
};
/**
* The unique id of each operation.
*/
export type OpId = { peer: PeerID, counter: number };
/**
* Change is a group of continuous operations
*/
export interface Change {
peer: PeerID,
counter: number,
lamport: number,
length: number,
timestamp: number,
deps: OpId[],
}
/**
* Data types supported by loro
*/
export type Value =
| ContainerID
| string
| number
| boolean
| null
| { [key: string]: Value }
| Uint8Array
| Value[];
export type UndoConfig = {
mergeInterval?: number,
maxUndoSteps?: number,
excludeOriginPrefixes?: string[],
onPush?: (isUndo: boolean, counterRange: { start: number, end: number }) => { value: Value, cursors: Cursor[] },
onPop?: (isUndo: boolean, value: { value: Value, cursors: Cursor[] }, counterRange: { start: number, end: number }) => void
};
export type Container = LoroList | LoroMap | LoroText | LoroTree | LoroMovableList;
export interface ImportBlobMetadata {
/**
* The version vector of the start of the import.
*
* Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
* However, it does not constitute a complete version vector, as it only contains counters
* from peers included within the import blob.
*/
partialStartVersionVector: VersionVector;
/**
* The version vector of the end of the import.
*
* Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
* However, it does not constitute a complete version vector, as it only contains counters
* from peers included within the import blob.
*/
partialEndVersionVector: VersionVector;
startFrontiers: OpId[],
startTimestamp: number;
endTimestamp: number;
isSnapshot: boolean;
changeNum: number;
}
interface LoroText {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new Loro();
* const text = doc.getText("text");
* text.insert(0, "123");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
interface LoroList {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new Loro();
* const text = doc.getList("list");
* text.insert(0, "1");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
export type TreeNodeValue = {
id: TreeID,
parent: TreeID | undefined,
index: number,
fractionalIndex: string,
meta: LoroMap,
}
interface LoroTree{
toArray(): TreeNodeValue[];
}
interface LoroMovableList {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new Loro();
* const text = doc.getMovableList("text");
* text.insert(0, "1");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
export type Side = -1 | 0 | 1;
"#;
#[wasm_bindgen(typescript_custom_section)]
const JSON_SCHEMA_TYPES: &'static str = r#"
export type JsonOpID = `${number}@${PeerID}`;
export type JsonContainerID = `🦜:${ContainerID}` ;
export type JsonValue =
| JsonContainerID
| string
| number
| boolean
| null
| { [key: string]: JsonValue }
| Uint8Array
| JsonValue[];
export type JsonSchema = {
schema_version: number;
start_version: Map<string, number>,
peers: PeerID[],
changes: JsonChange[]
};
export type JsonChange = {
id: JsonOpID
timestamp: number,
deps: JsonOpID[],
lamport: number,
msg: string | null,
ops: JsonOp[]
}
export type JsonOp = {
container: ContainerID,
counter: number,
content: ListOp | TextOp | MapOp | TreeOp | MovableListOp | UnknownOp
}
export type ListOp = {
type: "insert",
pos: number,
value: JsonValue
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
};
export type MovableListOp = {
type: "insert",
pos: number,
value: JsonValue
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
}| {
type: "move",
from: number,
to: number,
elem_id: JsonOpID,
}|{
type: "set",
elem_id: JsonOpID,
value: JsonValue
}
export type TextOp = {
type: "insert",
pos: number,
text: string
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
} | {
type: "mark",
start: number,
end: number,
style_key: string,
style_value: JsonValue,
info: number
}|{
type: "mark_end"
};
export type MapOp = {
type: "insert",
key: string,
value: JsonValue
} | {
type: "delete",
key: string,
};
export type TreeOp = {
type: "create",
target: TreeID,
parent: TreeID | undefined,
fractional_index: string
}|{
type: "move",
target: TreeID,
parent: TreeID | undefined,
fractional_index: string
}|{
type: "delete",
target: TreeID
};
export type UnknownOp = {
type: "unknown"
prop: number,
value_type: "unknown",
value: {
kind: number,
data: Uint8Array
}
};
"#;