diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 08c37882..5e3cae59 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -76,7 +76,7 @@ type JsResult = Result; /// /// @example /// ```ts -/// import { LoroDoc } import "loro-crdt" +/// import { LoroDoc } from "loro-crdt" /// /// const loro = new LoroDoc(); /// const text = loro.getText("text"); @@ -84,8 +84,6 @@ type JsResult = Result; /// 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 LoroDoc(Arc); @@ -332,7 +330,7 @@ impl ChangeMeta { impl LoroDoc { /// Create a new loro document. /// - /// New document will have random peer id. + /// New document will have a random peer id. #[wasm_bindgen(constructor)] pub fn new() -> Self { let doc = LoroDocInner::new(); @@ -372,12 +370,12 @@ impl LoroDoc { /// Set whether to record the timestamp of each change. Default is `false`. /// - /// If enabled, the Unix timestamp will be recorded for each change automatically. + /// If enabled, the Unix timestamp (in seconds) 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. + /// NOTE: Timestamps are forced to be in ascending order in the OpLog's history. /// 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")] @@ -387,7 +385,7 @@ impl LoroDoc { /// 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. + /// The default value is 1_000_000, the default unit is seconds. #[wasm_bindgen(js_name = "setChangeMergeInterval")] pub fn set_change_merge_interval(&self, interval: f64) { self.0.set_change_merge_interval(interval as i64); @@ -458,15 +456,17 @@ impl LoroDoc { Ok(()) } - /// Get a loro document from the snapshot. + /// Create a loro document from the snapshot. /// - /// @see You can check out what is the snapshot [here](#). + /// @see You can learn more [here](https://loro.dev/docs/tutorial/encoding). /// /// @example /// ```ts - /// import { LoroDoc } import "loro-crdt" + /// import { LoroDoc } from "loro-crdt" /// - /// const bytes = /* The bytes encoded from other loro document *\/; + /// const doc = new LoroDoc(); + /// // ... + /// const bytes = doc.export({ mode: "snapshot" }); /// const loro = LoroDoc.fromSnapshot(bytes); /// ``` /// @@ -484,7 +484,7 @@ impl LoroDoc { /// > 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`. + /// This method has the same effect as invoking `checkoutToLatest`. /// /// @example /// ```ts @@ -494,9 +494,9 @@ impl LoroDoc { /// const text = doc.getText("text"); /// const frontiers = doc.frontiers(); /// text.insert(0, "Hello World!"); - /// loro.checkout(frontiers); + /// doc.checkout(frontiers); /// // you need call `attach()` or `checkoutToLatest()` before changing the doc. - /// loro.attach(); + /// doc.attach(); /// text.insert(0, "Hi"); /// ``` pub fn attach(&mut self) { @@ -507,11 +507,9 @@ impl LoroDoc { /// /// > 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 + /// > In a detached state, the document is not editable by default, 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 { LoroDoc } from "loro-crdt"; @@ -520,11 +518,11 @@ impl LoroDoc { /// 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 + /// console.log(doc.isDetached()); // false + /// doc.checkout(frontiers); + /// console.log(doc.isDetached()); // true + /// doc.attach(); + /// console.log(doc.isDetached()); // false /// ``` /// #[wasm_bindgen(js_name = "isDetached")] @@ -543,7 +541,7 @@ impl LoroDoc { /// /// const doc = new LoroDoc(); /// doc.detach(); - /// console.log(doc.is_detached()); // true + /// console.log(doc.isDetached()); // true /// ``` pub fn detach(&self) { self.0.detach() @@ -568,7 +566,7 @@ impl LoroDoc { /// /// > 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 + /// > In a detached state, the document is not editable by default, and any `import` operations will be /// > recorded in the `OpLog` without being applied to the `DocState`. /// /// This has the same effect as `attach`. @@ -581,9 +579,9 @@ impl LoroDoc { /// const text = doc.getText("text"); /// const frontiers = doc.frontiers(); /// text.insert(0, "Hello World!"); - /// loro.checkout(frontiers); + /// doc.checkout(frontiers); /// // you need call `checkoutToLatest()` or `attach()` before changing the doc. - /// loro.checkoutToLatest(); + /// doc.checkoutToLatest(); /// text.insert(0, "Hi"); /// ``` #[wasm_bindgen(js_name = "checkoutToLatest")] @@ -592,7 +590,10 @@ impl LoroDoc { Ok(()) } + /// Visit all the ancestors of the changes in causal order. /// + /// @param ids - the changes to visit + /// @param f - the callback function, return `true` to continue visiting, return `false` to stop #[wasm_bindgen(js_name = "travelChangeAncestors")] pub fn travel_change_ancestors(&self, ids: Vec, f: js_sys::Function) -> JsResult<()> { let observer = observer::Observer::new(f); @@ -652,7 +653,7 @@ impl LoroDoc { /// const text = doc.getText("text"); /// const frontiers = doc.frontiers(); /// text.insert(0, "Hello World!"); - /// loro.checkout(frontiers); + /// doc.checkout(frontiers); /// console.log(doc.toJSON()); // {"text": ""} /// ``` pub fn checkout(&mut self, frontiers: Vec) -> JsResult<()> { @@ -690,7 +691,8 @@ impl LoroDoc { /// /// You can specify the `origin`, `timestamp`, and `message` of the commit. /// - /// The `origin` is used to mark the event, and the `message` works like a git commit message. + /// - The `origin` is used to mark the event + /// - The `message` works like a git commit message, which will be recorded and synced to peers /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// @@ -984,7 +986,7 @@ impl LoroDoc { Ok(ans) } - /// Get the encoded version vector of the current document. + /// Get the version vector of the current document state. /// /// If you checkout to a specific version, the version vector will change. #[inline(always)] @@ -1020,15 +1022,15 @@ impl LoroDoc { frontiers_to_ids(&self.0.shallow_since_frontiers()) } - /// Get the encoded version vector of the latest version in OpLog. + /// Get the version vector of the latest known version in OpLog. /// - /// If you checkout to a specific version, the version vector will not change. + /// If you checkout to a specific version, this 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. + /// Get the [frontiers](https://loro.dev/docs/advanced/version_deep_dive) of the current document state. /// /// If you checkout to a specific version, this value will change. #[inline] @@ -1036,7 +1038,7 @@ impl LoroDoc { frontiers_to_ids(&self.0.state_frontiers()) } - /// Get the frontiers of the latest version in OpLog. + /// Get the [frontiers](https://loro.dev/docs/advanced/version_deep_dive) of the latest version in OpLog. /// /// If you checkout to a specific version, this value will not change. #[inline(always)] @@ -1102,8 +1104,8 @@ impl LoroDoc { } } - /// Export the snapshot of current version, it's include all content of - /// operations and states + /// Export the snapshot of current version. + /// It includes all the history and the document state /// /// @deprecated Use `export({mode: "snapshot"})` instead #[wasm_bindgen(js_name = "exportSnapshot")] @@ -1141,7 +1143,7 @@ impl LoroDoc { /// /// @param mode - The export mode to use. Can be one of: /// - `{ mode: "snapshot" }`: Export a full snapshot of the document. - /// - `{ mode: "update", from: VersionVector }`: Export updates from the given version vector. + /// - `{ mode: "update", from?: VersionVector }`: Export updates from the given version vector. /// - `{ mode: "updates-in-range", spans: { id: ID, len: number }[] }`: Export updates within the specified ID spans. /// - `{ mode: "shallow-snapshot", frontiers: Frontiers }`: Export a garbage-collected snapshot up to the given frontiers. /// @@ -1149,34 +1151,36 @@ impl LoroDoc { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); - /// doc.setText("text", "Hello World"); + /// doc.setPeerId("1"); + /// doc.getText("text").update("Hello World"); /// /// // Export a full snapshot /// const snapshotBytes = doc.export({ mode: "snapshot" }); /// /// // Export updates from a specific version /// const vv = doc.oplogVersion(); - /// doc.setText("text", "Hello Loro"); + /// doc.getText("text").update("Hello Loro"); /// const updateBytes = doc.export({ mode: "update", from: vv }); /// - /// // Export a garbage-collected snapshot - /// const gcBytes = doc.export({ mode: "shallow-snapshot", frontiers: doc.oplogFrontiers() }); + /// // Export a shallow snapshot that only includes the history since the frontiers + /// const shallowBytes = doc.export({ mode: "shallow-snapshot", frontiers: doc.oplogFrontiers() }); /// /// // Export updates within specific ID spans /// const spanBytes = doc.export({ /// mode: "updates-in-range", - /// spans: [{ id: "1", len: 10 }, { id: "2", len: 5 }] + /// spans: [{ id: { peer: "1", counter: 0 }, len: 10 }] /// }); /// ``` pub fn export(&self, mode: JsExportMode) -> JsResult> { - let export_mode = js_to_export_mode(mode)?; + let export_mode = js_to_export_mode(mode) + .map_err(|e| JsValue::from_str(&format!("Invalid export mode. Error: {:?}", e)))?; Ok(self.0.export(export_mode)?) } - /// Export updates from the specific version to the current version with JSON format. + /// Export updates in the given range in JSON format. #[wasm_bindgen(js_name = "exportJsonUpdates")] pub fn export_json_updates( &self, @@ -1218,7 +1222,7 @@ impl LoroDoc { Ok(import_status_to_js_value(status).into()) } - /// Import a snapshot or a update to current doc. + /// Import snapshot or updates into current doc. /// /// Note: /// - Updates within the current version will be ignored @@ -1232,8 +1236,8 @@ impl LoroDoc { /// const text = doc.getText("text"); /// text.insert(0, "Hello"); /// // get all updates of the doc - /// const updates = doc.exportFrom(); - /// const snapshot = doc.exportSnapshot(); + /// const updates = doc.export({ mode: "update" }); + /// const snapshot = doc.export({ mode: "snapshot" }); /// const doc2 = new LoroDoc(); /// // import snapshot /// doc2.import(snapshot); @@ -1256,8 +1260,8 @@ impl LoroDoc { /// const doc = new LoroDoc(); /// const text = doc.getText("text"); /// text.insert(0, "Hello"); - /// const updates = doc.exportFrom(); - /// const snapshot = doc.exportSnapshot(); + /// const updates = doc.export({ mode: "update" }); + /// const snapshot = doc.export({ mode: "snapshot" }); /// const doc2 = new LoroDoc(); /// doc2.importUpdateBatch([snapshot, updates]); /// ``` @@ -1286,7 +1290,7 @@ impl LoroDoc { /// const list = doc.getList("list"); /// const tree = doc.getTree("tree"); /// const map = doc.getMap("map"); - /// const shallowValue = doc.toShallowJSON(); + /// const shallowValue = doc.getShallowValue(); /// /* /// {"list": ..., "tree": ..., "map": ...} /// *\/ @@ -1298,11 +1302,11 @@ impl LoroDoc { Ok(json.into()) } - /// Get the json format of the document state. + /// Get the json format of the entire document state. /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText, LoroMap } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// const list = doc.getList("list"); @@ -1323,14 +1327,14 @@ impl LoroDoc { } /// Subscribe to the changes of the loro document. The function will be called when the - /// transaction is committed or updates from remote are imported. + /// transaction is committed and after importing updates/snapshot from remote. /// - /// Returns a subscription ID, which can be used to unsubscribe. + /// Returns a subscription callback, which can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -1340,12 +1344,14 @@ impl LoroDoc { /// /// const doc = new LoroDoc(); /// const text = doc.getText("text"); - /// doc.subscribe((event)=>{ + /// const sub = doc.subscribe((event)=>{ /// console.log(event); /// }); /// text.insert(0, "Hello"); /// // the events will be emitted when `commit()` is called. /// doc.commit(); + /// // unsubscribe + /// sub(); /// ``` // TODO: convert event and event sub config pub fn subscribe(&self, f: js_sys::Function) -> JsValue { @@ -1382,20 +1388,22 @@ impl LoroDoc { console_log!("{:#?}", oplog.diagnose_size()); } - /// Get all of changes in the oplog + /// Get all of changes in the oplog. + /// + /// Note: this method is expensive when the oplog is large. O(n) /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// const text = doc.getText("text"); /// text.insert(0, "Hello"); /// const changes = doc.getAllChanges(); /// - /// for (let [peer, changes] of changes.entries()){ + /// for (let [peer, c] of changes.entries()){ /// console.log("peer: ", peer); - /// for (let change in changes){ + /// for (let change of c){ /// console.log("change: ", change); /// } /// } @@ -1438,7 +1446,7 @@ impl LoroDoc { value.into() } - /// Get the change of a specific ID + /// Get the change that contains the specific ID #[wasm_bindgen(js_name = "getChangeAt")] pub fn get_change_at(&self, id: JsID) -> JsResult { let id = js_id_to_id(id)?; @@ -1500,7 +1508,7 @@ impl LoroDoc { Ok(change.to_js().into()) } - /// Get all ops of the change of a specific ID + /// Get all ops of the change that contains the specific ID #[wasm_bindgen(js_name = "getOpsInChange")] pub fn get_ops_in_change(&self, id: JsID) -> JsResult> { let id = js_id_to_id(id)?; @@ -1517,7 +1525,9 @@ impl LoroDoc { Ok(ops) } - /// Convert frontiers to a readable version vector + /// Convert frontiers to a version vector + /// + /// Learn more about frontiers and version vector [here](https://loro.dev/docs/advanced/version_deep_dive) /// /// @example /// ```ts @@ -1730,34 +1740,6 @@ fn convert_container_path_to_js_value(path: &[(ContainerID, Index)]) -> Array { /// 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] @@ -1775,7 +1757,7 @@ struct MarkRange { #[wasm_bindgen] impl LoroText { - /// Create a new detached LoroText. + /// Create a new detached LoroText (not attached to any LoroDoc). /// /// The edits on a detached container will not be persisted. /// To attach the container to the document, please insert it into an attached container. @@ -1815,7 +1797,13 @@ impl LoroText { }) } - /// Update the current text based on the provided text. + /// Update the current text to the target text. + /// + /// It will calculate the minimal difference and apply it to the current text. + /// It uses Myers' diff algorithm to compute the optimal difference. + /// + /// This could take a long time for large texts (e.g. > 50_000 characters). + /// In that case, you should use `updateByLine` instead. /// /// @example /// ```ts @@ -1825,18 +1813,21 @@ impl LoroText { /// const text = doc.getText("text"); /// text.insert(0, "Hello"); /// text.update("Hello World"); + /// console.log(text.toString()); // "Hello World" /// ``` pub fn update(&self, text: &str) { self.handler.update(text); } - /// Update the current text based on the provided text line by line. + /// Update the current text to the target text, the difference is calculated line by line. + /// + /// It uses Myers' diff algorithm to compute the optimal difference. #[wasm_bindgen(js_name = "updateByLine")] pub fn update_by_line(&self, text: &str) { self.handler.update_by_line(text); } - /// Insert some string at index. + /// Insert the string at the given index (utf-16 index). /// /// @example /// ```ts @@ -1851,7 +1842,7 @@ impl LoroText { Ok(()) } - /// Get a string slice. + /// Get a string slice (utf-16 index). /// /// @example /// ```ts @@ -1869,7 +1860,7 @@ impl LoroText { } } - /// Get the character at the given position. + /// Get the character at the given position (utf-16 index). /// /// @example /// ```ts @@ -1888,7 +1879,7 @@ impl LoroText { } } - /// Delete and return the string at the given range and insert a string at the same position. + /// Delete and return the string at the given range and insert a string at the same position (utf-16 index). /// /// @example /// ```ts @@ -1922,7 +1913,7 @@ impl LoroText { Ok(()) } - /// Delete elements from index to index + len + /// Delete elements from index to index + len (utf-16 index). /// /// @example /// ```ts @@ -1959,7 +1950,7 @@ impl LoroText { Ok(()) } - /// Mark a range of text with a key and a value. + /// Mark a range of text with a key and a value (utf-16 index). /// /// > You should call `configTextStyle` before using `mark` and `unmark`. /// @@ -1984,7 +1975,7 @@ impl LoroText { Ok(()) } - /// Unmark a range of text with a key and a value. + /// Unmark a range of text with a key and a value (utf-16 index). /// /// > You should call `configTextStyle` before using `mark` and `unmark`. /// @@ -2008,7 +1999,7 @@ impl LoroText { Ok(()) } - /// Convert the state to string + /// Convert the text to a string #[allow(clippy::inherent_to_string)] #[wasm_bindgen(js_name = "toString")] pub fn to_string(&self) -> String { @@ -2053,7 +2044,7 @@ impl LoroText { value.into() } - /// Get the length of text + /// Get the length of text (utf-16 length). #[wasm_bindgen(js_name = "length", method, getter)] pub fn length(&self) -> usize { self.handler.len_utf16() @@ -2064,11 +2055,11 @@ impl LoroText { /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// - /// returns a subscription id, which can be used to unsubscribe. + /// returns a subscription callback, which can be used to unsubscribe. pub fn subscribe(&self, f: js_sys::Function) -> JsResult { let observer = observer::Observer::new(f); let doc = self @@ -2100,8 +2091,6 @@ impl LoroText { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; - /// /// const doc = new LoroDoc(); /// const text = doc.getText("text"); /// doc.configTextStyle({bold: {expand: "after"}}); @@ -2121,7 +2110,7 @@ impl LoroText { /// Get the parent container. /// - /// - The parent container of the root tree is `undefined`. + /// - The parent of the root 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 { @@ -2132,7 +2121,7 @@ impl LoroText { } } - /// Whether the container is attached to a document. + /// Whether the container is attached to a LoroDoc. /// /// If it's detached, the operations on the container will not be persisted. #[wasm_bindgen(js_name = "isAttached")] @@ -2142,7 +2131,7 @@ impl LoroText { /// Get the attached container associated with this. /// - /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + /// Returns an attached `Container` that is equal to this or created by this; otherwise, it returns `undefined`. #[wasm_bindgen(js_name = "getAttached")] pub fn get_attached(&self) -> JsLoroTextOrUndefined { if self.is_attached() { @@ -2157,7 +2146,10 @@ impl LoroText { } } - /// get the cursor at the given position. + /// Get the cursor at the given position. + /// + /// - The first argument is the position (utf16-index). + /// - The second argument is the side: `-1` for left, `0` for middle, `1` for right. #[wasm_bindgen(skip_typescript)] pub fn getCursor(&self, pos: usize, side: JsSide) -> Option { let mut side_value = Side::Middle; @@ -2189,7 +2181,7 @@ pub struct LoroMap { #[wasm_bindgen] impl LoroMap { - /// Create a new detached LoroMap. + /// Create a new detached LoroMap (not attached to any LoroDoc). /// /// The edits on a detached container will not be persisted. /// To attach the container to the document, please insert it into an attached container. @@ -2369,14 +2361,14 @@ impl LoroMap { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// 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"} + /// console.log(map.toJSON()); // {"foo": "bar", "text": "Hello"} /// ``` #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> JsValue { @@ -2387,7 +2379,7 @@ impl LoroMap { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// const map = doc.getMap("map"); @@ -2404,12 +2396,12 @@ impl LoroMap { /// Subscribe to the changes of the map. /// - /// Returns a subscription id, which can be used to unsubscribe. + /// Returns a subscription callback, which can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -2522,7 +2514,7 @@ pub struct LoroList { #[wasm_bindgen] impl LoroList { - /// Create a new detached LoroList. + /// Create a new detached LoroList (not attached to any LoroDoc). /// /// The edits on a detached container will not be persisted. /// To attach the container to the document, please insert it into an attached container. @@ -2613,7 +2605,7 @@ impl LoroList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// const list = doc.getList("list"); @@ -2646,14 +2638,14 @@ impl LoroList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// 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"]; + /// console.log(list.toJSON()); // [100, "Hello"]; /// ``` #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> JsValue { @@ -2665,14 +2657,14 @@ impl LoroList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// 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"]; + /// console.log(list.toJSON()); // [100, "Hello"]; /// ``` #[wasm_bindgen(js_name = "insertContainer", skip_typescript)] pub fn insert_container(&mut self, index: usize, child: JsContainer) -> JsResult { @@ -2683,12 +2675,12 @@ impl LoroList { /// Subscribe to the changes of the list. /// - /// Returns a subscription id, which can be used to unsubscribe. + /// Returns a subscription callback, which can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -2777,6 +2769,9 @@ impl LoroList { } /// Get the cursor at the position. + /// + /// - The first argument is the position . + /// - The second argument is the side: `-1` for left, `0` for middle, `1` for right. #[wasm_bindgen(skip_typescript)] pub fn getCursor(&self, pos: usize, side: JsSide) -> Option { let mut side_value = Side::Middle; @@ -2839,7 +2834,7 @@ impl Default for LoroMovableList { #[wasm_bindgen] impl LoroMovableList { - /// Create a new detached LoroList. + /// Create a new detached LoroMovableList (not attached to any LoroDoc). /// /// The edits on a detached container will not be persisted. /// To attach the container to the document, please insert it into an attached container. @@ -2930,7 +2925,7 @@ impl LoroMovableList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// const list = doc.getList("list"); @@ -2963,14 +2958,14 @@ impl LoroMovableList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// 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"]; + /// console.log(list.toJSON()); // [100, "Hello"]; /// ``` #[wasm_bindgen(js_name = "toJSON")] pub fn to_json(&self) -> JsValue { @@ -2982,14 +2977,14 @@ impl LoroMovableList { /// /// @example /// ```ts - /// import { LoroDoc } from "loro-crdt"; + /// import { LoroDoc, LoroText } from "loro-crdt"; /// /// const doc = new LoroDoc(); /// 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"]; + /// console.log(list.toJSON()); // [100, "Hello"]; /// ``` #[wasm_bindgen(js_name = "insertContainer", skip_typescript)] pub fn insert_container(&mut self, index: usize, child: JsContainer) -> JsResult { @@ -3000,12 +2995,12 @@ impl LoroMovableList { /// Subscribe to the changes of the list. /// - /// Returns a subscription id, which can be used to unsubscribe. + /// Returns a subscription callback, which can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -3271,10 +3266,10 @@ impl LoroTreeNode { /// ```ts /// const doc = new LoroDoc(); /// const tree = doc.getTree("tree"); - /// const root = tree.createChildNode(); - /// const node = root.createChildNode(); - /// const node2 = node.createChildNode(); - /// node2.moveTo(undefined, 0); + /// const root = tree.createNode(); + /// const node = root.createNode(); + /// const node2 = node.createNode(); + /// node2.move(undefined, 0); /// // node2 root /// // | /// // node @@ -3416,7 +3411,7 @@ impl LoroTreeNode { #[wasm_bindgen] impl LoroTree { - /// Create a new detached LoroTree. + /// Create a new detached LoroTree (not attached to any LoroDoc). /// /// The edits on a detached container will not be persisted. /// To attach the container to the document, please insert it into an attached container. @@ -3480,9 +3475,9 @@ impl LoroTree { /// const root = tree.createNode(); /// const node = root.createNode(); /// const node2 = node.createNode(); - /// tree.move(node2, root); + /// tree.move(node2.id, root.id); /// // Error will be thrown if move operation creates a cycle - /// tree.move(root, node); + /// // tree.move(root.id, node.id); /// ``` #[wasm_bindgen(js_name = "move")] pub fn mov( @@ -3677,7 +3672,7 @@ impl LoroTree { /// Subscribe to the changes of the tree. /// - /// Returns a subscription id, which can be used to unsubscribe. + /// Returns a subscription callback, 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, @@ -3695,7 +3690,7 @@ impl LoroTree { /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -3766,12 +3761,14 @@ impl LoroTree { } } - /// Set whether to generate fractional index for Tree Position. + /// Set whether to generate a fractional index for moving and creating. /// - /// 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. + /// A fractional index can be used to determine the position of tree nodes among their siblings. /// - /// Generally speaking, jitter will affect the growth rate of document size. + /// The jitter is used to avoid conflicts when multiple users are creating a node at the same position. + /// A value of 0 is the default, which means no jitter; any value larger than 0 will enable jitter. + /// + /// Generally speaking, higher jitter value will increase the size of the operation /// [Read more about it](https://www.loro.dev/blog/movable-tree#implementation-and-encoding-size) #[wasm_bindgen(js_name = "enableFractionalIndex")] pub fn enable_fractional_index(&self, jitter: u8) { @@ -3779,7 +3776,7 @@ impl LoroTree { } /// Disable the fractional index generation for Tree Position when - /// you don't need the Tree's siblings to be sorted. The fractional index will be always default. + /// you don't need the Tree's siblings to be sorted. The fractional index will always be set to default. #[wasm_bindgen(js_name = "disableFractionalIndex")] pub fn disable_fractional_index(&self) { self.handler.disable_fractional_index(); @@ -4385,13 +4382,12 @@ const TYPES: &'static str = r#" * It is most commonly used to specify the type of sub-container to be created. * @example * ```ts -* import { LoroDoc } from "loro-crdt"; +* import { LoroDoc, LoroText } from "loro-crdt"; * * const doc = new LoroDoc(); * const list = doc.getList("list"); * list.insert(0, 100); -* const containerType = "Text"; -* const text = list.insertContainer(1, containerType); +* const text = list.insertContainer(1, new LoroText()); * ``` */ export type ContainerType = "Text" | "Map" | "List"| "Tree" | "MovableList"; diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 4ba7425d..841b95f7 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -195,7 +195,7 @@ impl LoroDoc { self.doc.is_detached_editing_enabled() } - /// Set the interval of mergeable changes, in milliseconds. + /// Set the interval of mergeable changes, in seconds. /// /// If two continuous local changes are within the interval, they will be merged into one change. /// The default value is 1000 seconds. @@ -229,10 +229,10 @@ impl LoroDoc { /// 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`. + /// 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`. #[inline] @@ -358,7 +358,7 @@ impl LoroDoc { /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. #[inline] @@ -485,7 +485,7 @@ impl LoroDoc { self.doc.oplog_vv() } - /// Get the `VersionVector` version of `OpLog` + /// Get the `VersionVector` version of `DocState` #[inline] pub fn state_vv(&self) -> VersionVector { self.doc.state_vv() @@ -529,13 +529,13 @@ impl LoroDoc { self.doc.get_value() } - /// Get the current state of the document. + /// Get the entire state of the current DocState #[inline] pub fn get_deep_value(&self) -> LoroValue { self.doc.get_deep_value() } - /// Get the current state with container id of the doc + /// Get the entire state of the current DocState with container id pub fn get_deep_value_with_id(&self) -> LoroValue { self.doc .app_state() @@ -552,7 +552,7 @@ impl LoroDoc { /// Get the `Frontiers` version of `DocState` /// - /// [Learn more about `Frontiers`]() + /// Learn more about [`Frontiers`](https://loro.dev/docs/advanced/version_deep_dive) #[inline] pub fn state_frontiers(&self) -> Frontiers { self.doc.state_frontiers() @@ -566,7 +566,7 @@ impl LoroDoc { /// Change the PeerID /// - /// NOTE: You need ot make sure there is no chance two peer have the same PeerID. + /// NOTE: You need to make sure there is no chance two peer have the same PeerID. /// If it happens, the document will be corrupted. #[inline] pub fn set_peer_id(&self, peer: PeerID) -> LoroResult<()> { @@ -576,12 +576,12 @@ impl LoroDoc { /// Subscribe the events of a container. /// /// The callback will be invoked after a transaction that change the container. - /// Returns a subscription id that can be used to unsubscribe. + /// Returns a subscription that can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. /// @@ -614,6 +614,8 @@ impl LoroDoc { /// text.insert(0, "123").unwrap(); /// doc.commit(); /// assert!(ran.load(std::sync::atomic::Ordering::Relaxed)); + /// // unsubscribe + /// sub.unsubscribe(); /// ``` #[inline] pub fn subscribe(&self, container_id: &ContainerID, callback: Subscriber) -> Subscription { @@ -628,12 +630,12 @@ impl LoroDoc { /// Subscribe all the events. /// /// The callback will be invoked when any part of the [loro_internal::DocState] is changed. - /// Returns a subscription id that can be used to unsubscribe. + /// Returns a subscription that can be used to unsubscribe. /// /// The events will be emitted after a transaction is committed. A transaction is committed when: /// /// - `doc.commit()` is called. - /// - `doc.exportFrom(version)` is called. + /// - `doc.export(mode)` is called. /// - `doc.import(data)` is called. /// - `doc.checkout(version)` is called. #[inline] @@ -2266,7 +2268,7 @@ impl ContainerTrait for LoroUnknown { use enum_as_inner::EnumAsInner; -/// All the CRDT containers supported by loro. +/// All the CRDT containers supported by Loro. #[derive(Clone, Debug, EnumAsInner)] pub enum Container { /// [LoroList container](https://loro.dev/docs/tutorial/list) diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index 7804284c..bff99a9e 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -3,23 +3,23 @@ export type * from "loro-wasm"; import { Container, ContainerID, + ContainerType, Delta, + LoroCounter, LoroDoc, LoroList, LoroMap, LoroText, LoroTree, - LoroCounter, OpId, TreeID, Value, - ContainerType, } from "loro-wasm"; /** * @deprecated Please use LoroDoc */ -export class Loro extends LoroDoc { } +export class Loro extends LoroDoc {} export { Awareness } from "./awareness"; export type Frontiers = OpId[]; @@ -97,7 +97,12 @@ export type TreeDiffItem = index: number; fractionalIndex: string; } - | { target: TreeID; action: "delete"; oldParent: TreeID | undefined; oldIndex: number } + | { + target: TreeID; + action: "delete"; + oldParent: TreeID | undefined; + oldIndex: number; + } | { target: TreeID; action: "move"; @@ -116,7 +121,7 @@ export type TreeDiff = { export type CounterDiff = { type: "counter"; increment: number; -} +}; export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff | CounterDiff; @@ -124,7 +129,14 @@ interface Listener { (event: LoroEventBatch): void; } -const CONTAINER_TYPES = ["Map", "Text", "List", "Tree", "MovableList", "Counter"]; +const CONTAINER_TYPES = [ + "Map", + "Text", + "List", + "Tree", + "MovableList", + "Counter", +]; export function isContainerId(s: string): s is ContainerID { return s.startsWith("cid:"); @@ -145,6 +157,7 @@ export function isContainerId(s: string): s is ContainerID { * isContainer(123); // false * isContainer("123"); // false * isContainer({}); // false + * ``` */ export function isContainer(value: any): value is Container { if (typeof value !== "object" || value == null) { @@ -178,14 +191,10 @@ export function isContainer(value: any): value is Container { */ export function getType( value: T, -): T extends LoroText - ? "Text" - : T extends LoroMap - ? "Map" - : T extends LoroTree - ? "Tree" - : T extends LoroList - ? "List" +): T extends LoroText ? "Text" + : T extends LoroMap ? "Map" + : T extends LoroTree ? "Tree" + : T extends LoroList ? "List" : T extends LoroCounter ? "Counter" : "Json" { if (isContainer(value)) { @@ -293,14 +302,14 @@ declare module "loro-wasm" { } interface LoroList { - new(): LoroList; + new (): LoroList; /** * Get elements of the list. If the value is a child container, the corresponding * `Container` will be returned. * * @example * ```ts - * import { LoroDoc } from "loro-crdt"; + * import { LoroDoc, LoroText } from "loro-crdt"; * * const doc = new LoroDoc(); * const list = doc.getList("list"); @@ -369,7 +378,7 @@ declare module "loro-wasm" { } interface LoroMovableList { - new(): LoroMovableList; + new (): LoroMovableList; /** * Get elements of the list. If the value is a child container, the corresponding * `Container` will be returned. @@ -393,7 +402,7 @@ declare module "loro-wasm" { * * @example * ```ts - * import { LoroDoc } from "loro-crdt"; + * import { LoroDoc, LoroText } from "loro-crdt"; * * const doc = new LoroDoc(); * const list = doc.getMovableList("list"); @@ -458,7 +467,7 @@ declare module "loro-wasm" { * import { LoroDoc } from "loro-crdt"; * * const doc = new LoroDoc(); - * const list = doc.getList("list"); + * const list = doc.getMovableList("list"); * list.insert(0, 100); * list.insert(1, "foo"); * list.insert(2, true); @@ -472,7 +481,7 @@ declare module "loro-wasm" { * * @example * ```ts - * import { LoroDoc } from "loro-crdt"; + * import { LoroDoc, LoroText } from "loro-crdt"; * * const doc = new LoroDoc(); * const list = doc.getMovableList("list"); @@ -491,7 +500,7 @@ declare module "loro-wasm" { interface LoroMap< T extends Record = Record, > { - new(): LoroMap; + new (): LoroMap; /** * Get the value of the key. If the value is a child container, the corresponding * `Container` will be returned. @@ -514,13 +523,13 @@ declare module "loro-wasm" { * * @example * ```ts - * import { LoroDoc } from "loro-crdt"; + * import { LoroDoc, LoroText, LoroList } from "loro-crdt"; * * const doc = new LoroDoc(); * const map = doc.getMap("map"); * map.set("foo", "bar"); * const text = map.setContainer("text", new LoroText()); - * const list = map.setContainer("list", new LoroText()); + * const list = map.setContainer("list", new LoroList()); * ``` */ setContainer( @@ -569,7 +578,7 @@ declare module "loro-wasm" { } interface LoroText { - new(): LoroText; + new (): LoroText; insert(pos: number, text: string): void; delete(pos: number, len: number): void; subscribe(listener: Listener): Subscription; @@ -578,7 +587,7 @@ declare module "loro-wasm" { interface LoroTree< T extends Record = Record, > { - new(): LoroTree; + new (): LoroTree; /** * 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. @@ -617,7 +626,7 @@ declare module "loro-wasm" { * Get the associated metadata map container of a tree node. */ readonly data: LoroMap; - /** + /** * Create a new node as the child of the current node and * return an instance of `LoroTreeNode`. * @@ -664,6 +673,9 @@ export function newContainerID(id: OpId, type: ContainerType): ContainerID { return `cid:${id.counter}@${id.peer}:${type}`; } -export function newRootContainerID(name: string, type: ContainerType): ContainerID { +export function newRootContainerID( + name: string, + type: ContainerType, +): ContainerID { return `cid:root-${name}:${type}`; } diff --git a/scripts/deno.json b/scripts/deno.json index c087ca80..0dbf8bc2 100644 --- a/scripts/deno.json +++ b/scripts/deno.json @@ -2,5 +2,8 @@ "imports": { "@std/fs": "jsr:@std/fs@^1.0.2", "@std/toml": "jsr:@std/toml@^1.0.1" + }, + "tasks": { + "doc-test": "deno run --allow-env --allow-read --allow-run run-js-doc-tests.ts ../crates/loro-wasm/src/lib.rs ../loro-js/src/index.ts" } } diff --git a/scripts/deno.lock b/scripts/deno.lock index 4f776732..1a27e806 100644 --- a/scripts/deno.lock +++ b/scripts/deno.lock @@ -1,34 +1,319 @@ { - "version": "3", - "packages": { - "specifiers": { - "jsr:@std/collections@^1.0.5": "jsr:@std/collections@1.0.5", - "jsr:@std/fs@^1.0.2": "jsr:@std/fs@1.0.2", - "jsr:@std/path@^1.0.3": "jsr:@std/path@1.0.3", - "jsr:@std/toml@^1.0.1": "jsr:@std/toml@1.0.1" + "version": "4", + "specifiers": { + "jsr:@std/collections@^1.0.5": "1.0.5", + "jsr:@std/fs@^1.0.2": "1.0.2", + "jsr:@std/path@^1.0.3": "1.0.3", + "jsr:@std/toml@^1.0.1": "1.0.1", + "npm:expect@29.7.0": "29.7.0", + "npm:loro-crdt@1.0.7": "1.0.7" + }, + "jsr": { + "@std/collections@1.0.5": { + "integrity": "ab9eac23b57a0c0b89ba45134e61561f69f3d001f37235a248ed40be260c0c10" }, - "jsr": { - "@std/collections@1.0.5": { - "integrity": "ab9eac23b57a0c0b89ba45134e61561f69f3d001f37235a248ed40be260c0c10" - }, - "@std/fs@1.0.2": { - "integrity": "af57555c7a224a6f147d5cced5404692974f7a628ced8eda67e0d22d92d474ec", - "dependencies": [ - "jsr:@std/path@^1.0.3" - ] - }, - "@std/path@1.0.3": { - "integrity": "cd89d014ce7eb3742f2147b990f6753ee51d95276bfc211bc50c860c1bc7df6f" - }, - "@std/toml@1.0.1": { - "integrity": "b55b407159930f338d384b1f8fd317c8e8a35e27ebb8946155f49e3a158d16c4", - "dependencies": [ - "jsr:@std/collections@^1.0.5" - ] - } + "@std/fs@1.0.2": { + "integrity": "af57555c7a224a6f147d5cced5404692974f7a628ced8eda67e0d22d92d474ec", + "dependencies": [ + "jsr:@std/path" + ] + }, + "@std/path@1.0.3": { + "integrity": "cd89d014ce7eb3742f2147b990f6753ee51d95276bfc211bc50c860c1bc7df6f" + }, + "@std/toml@1.0.1": { + "integrity": "b55b407159930f338d384b1f8fd317c8e8a35e27ebb8946155f49e3a158d16c4", + "dependencies": [ + "jsr:@std/collections" + ] + } + }, + "npm": { + "@babel/code-frame@7.25.9": { + "integrity": "sha512-z88xeGxnzehn2sqZ8UdGQEvYErF1odv2CftxInpSYJt6uHuPe9YjahKZITGs3l5LeI9d2ROG+obuDAoSlqbNfQ==", + "dependencies": [ + "@babel/highlight", + "picocolors" + ] + }, + "@babel/helper-validator-identifier@7.25.9": { + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/highlight@7.25.9": { + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dependencies": [ + "@babel/helper-validator-identifier", + "chalk@2.4.2", + "js-tokens", + "picocolors" + ] + }, + "@jest/expect-utils@29.7.0": { + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dependencies": [ + "jest-get-type" + ] + }, + "@jest/schemas@29.6.3": { + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": [ + "@sinclair/typebox" + ] + }, + "@jest/types@29.6.3": { + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": [ + "@jest/schemas", + "@types/istanbul-lib-coverage", + "@types/istanbul-reports", + "@types/node", + "@types/yargs", + "chalk@4.1.2" + ] + }, + "@sinclair/typebox@0.27.8": { + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + }, + "@types/istanbul-lib-coverage@2.0.6": { + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "@types/istanbul-lib-report@3.0.3": { + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dependencies": [ + "@types/istanbul-lib-coverage" + ] + }, + "@types/istanbul-reports@3.0.4": { + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": [ + "@types/istanbul-lib-report" + ] + }, + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "@types/stack-utils@2.0.3": { + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "@types/yargs-parser@21.0.3": { + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, + "@types/yargs@17.0.33": { + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dependencies": [ + "@types/yargs-parser" + ] + }, + "ansi-styles@3.2.1": { + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": [ + "color-convert@1.9.3" + ] + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert@2.0.1" + ] + }, + "ansi-styles@5.2.0": { + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==" + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, + "chalk@2.4.2": { + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": [ + "ansi-styles@3.2.1", + "escape-string-regexp@1.0.5", + "supports-color@5.5.0" + ] + }, + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles@4.3.0", + "supports-color@7.2.0" + ] + }, + "ci-info@3.9.0": { + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==" + }, + "color-convert@1.9.3": { + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": [ + "color-name@1.1.3" + ] + }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name@1.1.4" + ] + }, + "color-name@1.1.3": { + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "diff-sequences@29.6.3": { + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==" + }, + "escape-string-regexp@1.0.5": { + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + }, + "escape-string-regexp@2.0.0": { + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + }, + "expect@29.7.0": { + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": [ + "@jest/expect-utils", + "jest-get-type", + "jest-matcher-utils", + "jest-message-util", + "jest-util" + ] + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] + }, + "graceful-fs@4.2.11": { + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has-flag@3.0.0": { + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "jest-diff@29.7.0": { + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": [ + "chalk@4.1.2", + "diff-sequences", + "jest-get-type", + "pretty-format" + ] + }, + "jest-get-type@29.6.3": { + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==" + }, + "jest-matcher-utils@29.7.0": { + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": [ + "chalk@4.1.2", + "jest-diff", + "jest-get-type", + "pretty-format" + ] + }, + "jest-message-util@29.7.0": { + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": [ + "@babel/code-frame", + "@jest/types", + "@types/stack-utils", + "chalk@4.1.2", + "graceful-fs", + "micromatch", + "pretty-format", + "slash", + "stack-utils" + ] + }, + "jest-util@29.7.0": { + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": [ + "@jest/types", + "@types/node", + "chalk@4.1.2", + "ci-info", + "graceful-fs", + "picomatch" + ] + }, + "js-tokens@4.0.0": { + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "loro-crdt@1.0.7": { + "integrity": "sha512-BD9qx3dQdqhEegOTEYdYo2GvRoYAeZFAfwcJLDtNRKo20y/1dUsRmzAvtLzZI/cdcRg+DkpBBLr+wcL4jRYUNw==", + "dependencies": [ + "loro-wasm" + ] + }, + "loro-wasm@1.0.7": { + "integrity": "sha512-WFIpGGzc6I7zRMDoRGxa3AHhno7gVnOgwqcrTfmpKWOtktZQ7BvhIV4kYgsdyuIBcMSrQEJTfOY/80xQSjUKTw==" + }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch" + ] + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pretty-format@29.7.0": { + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": [ + "@jest/schemas", + "ansi-styles@5.2.0", + "react-is" + ] + }, + "react-is@18.3.1": { + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "slash@3.0.0": { + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "stack-utils@2.0.6": { + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": [ + "escape-string-regexp@2.0.0" + ] + }, + "supports-color@5.5.0": { + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": [ + "has-flag@3.0.0" + ] + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag@4.0.0" + ] + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" } }, - "remote": {}, "workspace": { "dependencies": [ "jsr:@std/fs@^1.0.2", diff --git a/scripts/doc-tests-tests/example.txt b/scripts/doc-tests-tests/example.txt new file mode 100644 index 00000000..98811d32 --- /dev/null +++ b/scripts/doc-tests-tests/example.txt @@ -0,0 +1,113 @@ +/* tslint:disable */ +/* eslint-disable */ +/** */ +export function run(): void; +/** + * @param {({ peer: PeerID, counter: number })[]} frontiers + * @returns {Uint8Array} + */ +export function encodeFrontiers( + frontiers: ({ peer: PeerID; counter: number })[], +): Uint8Array; +/** + * @param {Uint8Array} bytes + * @returns {{ peer: PeerID, counter: number }[]} + */ +export function decodeFrontiers( + bytes: Uint8Array, +): { peer: PeerID; counter: number }[]; +/** + * Enable debug info of Loro + */ +export function setDebug(): void; +/** + * 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 + * @param {Uint8Array} blob + * @returns {ImportBlobMetadata} + */ +export function decodeImportBlobMeta(blob: Uint8Array): ImportBlobMetadata; + +/** + * Container types supported by loro. + * + * It is most commonly used to specify the type of sub-container to be created. + * @example + * ```ts + * import { LoroDoc, LoroText } from "loro-crdt"; + * + * const doc = new LoroDoc(); + * const list = doc.getList("list"); + * list.insert(0, 100); + * const text = list.insertContainer(1, new LoroText()); + * ``` + */ +export type ContainerType = "Text" | "Map" | "List" | "Tree" | "MovableList"; + +export type PeerID = `${number}`; +/** + * The unique id of each container. + * + * @example + * ```ts + * import { LoroDoc } from "loro-crdt"; + * + * const doc = new LoroDoc(); + * 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 LoroDoc { + /** + * Export updates from the specific version to the current version + * + * @deprecated Use `export({mode: "update", from: version})` instead + * + * @example + * ```ts + * import { LoroDoc } from "loro-crdt"; + * + * const doc = new LoroDoc(); + * 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 { LoroDoc } from "loro-crdt"; + /// + /// const doc = new LoroDoc(); + /// let text = doc.getText("text"); + /// const textId = text.id; + /// text = doc.getContainerById(textId); + /// ``` + /// + getContainerById(id: ContainerID): Container; +} diff --git a/scripts/run-js-doc-tests.ts b/scripts/run-js-doc-tests.ts new file mode 100644 index 00000000..360c6d3a --- /dev/null +++ b/scripts/run-js-doc-tests.ts @@ -0,0 +1,117 @@ +const LORO_VERSION = "1.0.7"; + +export interface CodeBlock { + filename: string; + filePath: string; + lineNumber: number; + lang: string; + content: string; +} + +export function extractCodeBlocks( + fileContent: string, + codeBlocks: CodeBlock[], + name: string, + path: string, +) { + // Regular expression to detect TypeScript code blocks + const codeBlockRegex = /```(typescript|ts|js|javascript)\n([\s\S]*?)```/g; + let match; + while ((match = codeBlockRegex.exec(fileContent)) !== null) { + const startLine = + fileContent.substring(0, match.index).split("\n").length; + let content = match[2]; + content = content.replace(/^\s*\*/g, ""); + content = content.replace(/\n\s*\*/g, "\n"); + content = content.replace(/^\s*\/\/\//g, ""); + content = content.replace(/\n\s*\/\/\//g, "\n"); + content = replaceImportVersion(content, LORO_VERSION); + if (!content.includes("loro-crdt")) { + content = IMPORTS + content; + } + codeBlocks.push({ + filename: name, + filePath: path, + lineNumber: startLine, + content, + lang: match[1], + }); + } +} + +function replaceImportVersion(input: string, targetVersion: string): string { + const regex = /from "loro-crdt"/g; + const replacement = `from "npm:loro-crdt@${targetVersion}"`; + return input.replace(regex, replacement); +} + +const IMPORTS = + `import { Loro, LoroDoc, LoroMap, LoroText, LoroList, Delta, UndoManager, getType, isContainer } from "npm:loro-crdt@${LORO_VERSION}"; +import { expect } from "npm:expect@29.7.0";\n +`; + +Deno.test("extract doc tests", async () => { + const filePath = "./doc-tests-tests/example.txt"; + const fileContent = await Deno.readTextFile(filePath); + const codeBlocks: CodeBlock[] = []; + extractCodeBlocks(fileContent, codeBlocks, "example.txt", filePath); + for (const block of codeBlocks) { + console.log(block.content); + console.log("=============================="); + } + await runCodeBlocks(codeBlocks); +}); + +export async function runDocTests(paths: string[]) { + const codeBlocks: CodeBlock[] = []; + for (const path of paths) { + const fileContent = await Deno.readTextFile(path); + extractCodeBlocks(fileContent, codeBlocks, path, path); + } + + await runCodeBlocks(codeBlocks); +} + +async function runCodeBlocks(codeBlocks: CodeBlock[]) { + let testCases = 0; + let passed = 0; + let failed = 0; + for (const block of codeBlocks) { + try { + const command = new Deno.Command("deno", { + args: ["eval", "--ext=ts", block.content], + stdout: "null", + stderr: "inherit", + }); + const process = command.spawn(); + const status = await process.status; + testCases += 1; + if (status.success) { + passed += 1; + } else { + console.log("----------------"); + console.log(block.content); + console.log("-----------------"); + console.error( + `\x1b[31;1mError in \x1b[4m${block.filePath}:${block.lineNumber}\x1b[0m\n\n\n\n\n`, + ); + failed += 1; + } + } catch (error) { + console.error("Error:", error); + } + await Deno.stdout.write( + new TextEncoder().encode( + `\r🧪 ${testCases} tests, ✅ ${passed} passed,${ + failed > 0 ? " ❌" : "" + } ${failed} failed`, + ), + ); + } +} + +if (Deno.args.length > 0) { + await runDocTests(Deno.args); +} else { + console.log("No paths provided. Please provide paths as arguments."); +}