diff --git a/Sources/Loro/LoroFFI.swift b/Sources/Loro/LoroFFI.swift index 797b6ce..f3a7631 100644 --- a/Sources/Loro/LoroFFI.swift +++ b/Sources/Loro/LoroFFI.swift @@ -1403,6 +1403,172 @@ public func FfiConverterTypeCursor_lower(_ value: Cursor) -> UnsafeMutableRawPoi +public protocol DiffBatchProtocol : AnyObject { + + /** + * Returns an iterator over the diffs in this batch, in the order they were added. + * + * The iterator yields tuples of `(&ContainerID, &Diff)` where: + * - `ContainerID` is the ID of the container that was modified + * - `Diff` contains the actual changes made to that container + * + * The order of the diffs is preserved from when they were originally added to the batch. + */ + func getDiff() -> [ContainerIdAndDiff] + + /** + * Push a new event to the batch. + * + * If the cid already exists in the batch, return Err + */ + func push(cid: ContainerId, diff: Diff) -> Diff? + +} + +open class DiffBatch: + DiffBatchProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + // This constructor can be used to instantiate a fake object. + // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + // + // - Warning: + // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public init(noPointer: NoPointer) { + self.pointer = nil + } + +#if swift(>=5.8) + @_documentation(visibility: private) +#endif + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_loro_fn_clone_diffbatch(self.pointer, $0) } + } +public convenience init() { + let pointer = + try! rustCall() { + uniffi_loro_fn_constructor_diffbatch_new($0 + ) +} + self.init(unsafeFromRawPointer: pointer) +} + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_loro_fn_free_diffbatch(pointer, $0) } + } + + + + + /** + * Returns an iterator over the diffs in this batch, in the order they were added. + * + * The iterator yields tuples of `(&ContainerID, &Diff)` where: + * - `ContainerID` is the ID of the container that was modified + * - `Diff` contains the actual changes made to that container + * + * The order of the diffs is preserved from when they were originally added to the batch. + */ +open func getDiff() -> [ContainerIdAndDiff] { + return try! FfiConverterSequenceTypeContainerIDAndDiff.lift(try! rustCall() { + uniffi_loro_fn_method_diffbatch_get_diff(self.uniffiClonePointer(),$0 + ) +}) +} + + /** + * Push a new event to the batch. + * + * If the cid already exists in the batch, return Err + */ +open func push(cid: ContainerId, diff: Diff) -> Diff? { + return try! FfiConverterOptionTypeDiff.lift(try! rustCall() { + uniffi_loro_fn_method_diffbatch_push(self.uniffiClonePointer(), + FfiConverterTypeContainerID.lower(cid), + FfiConverterTypeDiff.lower(diff),$0 + ) +}) +} + + +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeDiffBatch: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = DiffBatch + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> DiffBatch { + return DiffBatch(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: DiffBatch) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DiffBatch { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: DiffBatch, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDiffBatch_lift(_ pointer: UnsafeMutableRawPointer) throws -> DiffBatch { + return try FfiConverterTypeDiffBatch.lift(pointer) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeDiffBatch_lower(_ value: DiffBatch) -> UnsafeMutableRawPointer { + return FfiConverterTypeDiffBatch.lower(value) +} + + + + public protocol FractionalIndexProtocol : AnyObject { func toString() -> String @@ -2066,6 +2232,13 @@ public func FfiConverterTypeLoroCounter_lower(_ value: LoroCounter) -> UnsafeMut public protocol LoroDocProtocol : AnyObject { + /** + * Apply a diff to the current document state. + * + * Internally, it will apply the diff to the current state. + */ + func applyDiff(diff: DiffBatch) throws + /** * Attach the document state to the latest known version. * @@ -2160,11 +2333,29 @@ public protocol LoroDocProtocol : AnyObject { */ func detach() + /** + * Calculate the diff between two versions + */ + func diff(a: Frontiers, b: Frontiers) throws -> DiffBatch + + /** + * Export the readable [`Change`]s in the given [`IdSpan`] + */ + func exportJsonInIdSpan(idSpan: IdSpan) -> [String] + /** * Export the current state with json-string format of the document. */ func exportJsonUpdates(startVv: VersionVector, endVv: VersionVector) -> String + /** + * Export the current state with json-string format of the document, without peer compression. + * + * Compared to [`export_json_updates`], this method does not compress the peer IDs in the updates. + * So the operations are easier to be processed by application code. + */ + func exportJsonUpdatesWithoutPeerCompression(startVv: VersionVector, endVv: VersionVector) -> String + func exportShallowSnapshot(frontiers: Frontiers) throws -> Data /** @@ -2183,6 +2374,11 @@ public protocol LoroDocProtocol : AnyObject { func exportUpdatesInRange(spans: [IdSpan]) throws -> Data + /** + * Find the operation id spans that between the `from` version and the `to` version. + */ + func findIdSpansBetween(from: Frontiers, to: Frontiers) -> VersionVectorDiff + /** * Duplicate the document with a different PeerID * @@ -2224,7 +2420,57 @@ public protocol LoroDocProtocol : AnyObject { func getByPath(path: [Index]) -> ValueOrContainer? /** - * Get the handler by the string path. + * The path can be specified in different ways depending on the container type: + * + * For Tree: + * 1. Using node IDs: `tree/{node_id}/property` + * 2. Using indices: `tree/0/1/property` + * + * For List and MovableList: + * - Using indices: `list/0` or `list/1/property` + * + * For Map: + * - Using keys: `map/key` or `map/nested/property` + * + * For tree structures, index-based paths follow depth-first traversal order. + * The indices start from 0 and represent the position of a node among its siblings. + * + * # Examples + * ``` + * # use loro::{LoroDoc, LoroValue}; + * let doc = LoroDoc::new(); + * + * // Tree example + * let tree = doc.get_tree("tree"); + * let root = tree.create(None).unwrap(); + * tree.get_meta(root).unwrap().insert("name", "root").unwrap(); + * // Access tree by ID or index + * let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap(); + * let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap(); + * assert_eq!(name1, name2); + * + * // List example + * let list = doc.get_list("list"); + * list.insert(0, "first").unwrap(); + * list.insert(1, "second").unwrap(); + * // Access list by index + * let item = doc.get_by_str_path("list/0"); + * assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into()); + * + * // Map example + * let map = doc.get_map("map"); + * map.insert("key", "value").unwrap(); + * // Access map by key + * let value = doc.get_by_str_path("map/key"); + * assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into()); + * + * // MovableList example + * let mlist = doc.get_movable_list("mlist"); + * mlist.insert(0, "item").unwrap(); + * // Access movable list by index + * let item = doc.get_by_str_path("mlist/0"); + * assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into()); + * ``` */ func getByStrPath(path: String) -> ValueOrContainer? @@ -2323,7 +2569,7 @@ public protocol LoroDocProtocol : AnyObject { /** * Get a [LoroTree] by container id. * - * If the provided id is string, it will be converted into a root container id with the name of the string. + * If the provided id is string, it will be converted into a root container id with the name of the string. */ func getTree(id: ContainerIdLike) -> LoroTree @@ -2436,13 +2682,30 @@ public protocol LoroDocProtocol : AnyObject { func peerId() -> UInt64 /** - * Set the interval of mergeable changes, in seconds. + * Revert the current document state back to the target version + * + * Internally, it will generate a series of local operations that can revert the + * current doc to the target version. It will calculate the diff between the current + * state and the target state, and apply the diff to the current state. + */ + func revertTo(version: Frontiers) throws + + /** + * 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. + * + * By default, we record timestamps in seconds for each change. So if the merge interval is 1, and changes A and B + * have timestamps of 3 and 4 respectively, then they will be merged into one change */ func setChangeMergeInterval(interval: Int64) + /** + * Set commit message for the current uncommitted changes + * + * It will be persisted. + */ func setNextCommitMessage(msg: String) /** @@ -2593,6 +2856,18 @@ public convenience init() { + /** + * Apply a diff to the current document state. + * + * Internally, it will apply the diff to the current state. + */ +open func applyDiff(diff: DiffBatch)throws {try rustCallWithError(FfiConverterTypeLoroError.lift) { + uniffi_loro_fn_method_lorodoc_apply_diff(self.uniffiClonePointer(), + FfiConverterTypeDiffBatch.lower(diff),$0 + ) +} +} + /** * Attach the document state to the latest known version. * @@ -2735,6 +3010,29 @@ open func detach() {try! rustCall() { uniffi_loro_fn_method_lorodoc_detach(self.uniffiClonePointer(),$0 ) } +} + + /** + * Calculate the diff between two versions + */ +open func diff(a: Frontiers, b: Frontiers)throws -> DiffBatch { + return try FfiConverterTypeDiffBatch.lift(try rustCallWithError(FfiConverterTypeLoroError.lift) { + uniffi_loro_fn_method_lorodoc_diff(self.uniffiClonePointer(), + FfiConverterTypeFrontiers.lower(a), + FfiConverterTypeFrontiers.lower(b),$0 + ) +}) +} + + /** + * Export the readable [`Change`]s in the given [`IdSpan`] + */ +open func exportJsonInIdSpan(idSpan: IdSpan) -> [String] { + return try! FfiConverterSequenceString.lift(try! rustCall() { + uniffi_loro_fn_method_lorodoc_export_json_in_id_span(self.uniffiClonePointer(), + FfiConverterTypeIdSpan.lower(idSpan),$0 + ) +}) } /** @@ -2747,6 +3045,21 @@ open func exportJsonUpdates(startVv: VersionVector, endVv: VersionVector) -> Str FfiConverterTypeVersionVector.lower(endVv),$0 ) }) +} + + /** + * Export the current state with json-string format of the document, without peer compression. + * + * Compared to [`export_json_updates`], this method does not compress the peer IDs in the updates. + * So the operations are easier to be processed by application code. + */ +open func exportJsonUpdatesWithoutPeerCompression(startVv: VersionVector, endVv: VersionVector) -> String { + return try! FfiConverterString.lift(try! rustCall() { + uniffi_loro_fn_method_lorodoc_export_json_updates_without_peer_compression(self.uniffiClonePointer(), + FfiConverterTypeVersionVector.lower(startVv), + FfiConverterTypeVersionVector.lower(endVv),$0 + ) +}) } open func exportShallowSnapshot(frontiers: Frontiers)throws -> Data { @@ -2800,6 +3113,18 @@ open func exportUpdatesInRange(spans: [IdSpan])throws -> Data { FfiConverterSequenceTypeIdSpan.lower(spans),$0 ) }) +} + + /** + * Find the operation id spans that between the `from` version and the `to` version. + */ +open func findIdSpansBetween(from: Frontiers, to: Frontiers) -> VersionVectorDiff { + return try! FfiConverterTypeVersionVectorDiff.lift(try! rustCall() { + uniffi_loro_fn_method_lorodoc_find_id_spans_between(self.uniffiClonePointer(), + FfiConverterTypeFrontiers.lower(from), + FfiConverterTypeFrontiers.lower(to),$0 + ) +}) } /** @@ -2874,7 +3199,57 @@ open func getByPath(path: [Index]) -> ValueOrContainer? { } /** - * Get the handler by the string path. + * The path can be specified in different ways depending on the container type: + * + * For Tree: + * 1. Using node IDs: `tree/{node_id}/property` + * 2. Using indices: `tree/0/1/property` + * + * For List and MovableList: + * - Using indices: `list/0` or `list/1/property` + * + * For Map: + * - Using keys: `map/key` or `map/nested/property` + * + * For tree structures, index-based paths follow depth-first traversal order. + * The indices start from 0 and represent the position of a node among its siblings. + * + * # Examples + * ``` + * # use loro::{LoroDoc, LoroValue}; + * let doc = LoroDoc::new(); + * + * // Tree example + * let tree = doc.get_tree("tree"); + * let root = tree.create(None).unwrap(); + * tree.get_meta(root).unwrap().insert("name", "root").unwrap(); + * // Access tree by ID or index + * let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap(); + * let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap(); + * assert_eq!(name1, name2); + * + * // List example + * let list = doc.get_list("list"); + * list.insert(0, "first").unwrap(); + * list.insert(1, "second").unwrap(); + * // Access list by index + * let item = doc.get_by_str_path("list/0"); + * assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into()); + * + * // Map example + * let map = doc.get_map("map"); + * map.insert("key", "value").unwrap(); + * // Access map by key + * let value = doc.get_by_str_path("map/key"); + * assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into()); + * + * // MovableList example + * let mlist = doc.get_movable_list("mlist"); + * mlist.insert(0, "item").unwrap(); + * // Access movable list by index + * let item = doc.get_by_str_path("mlist/0"); + * assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into()); + * ``` */ open func getByStrPath(path: String) -> ValueOrContainer? { return try! FfiConverterOptionTypeValueOrContainer.lift(try! rustCall() { @@ -3049,7 +3424,7 @@ open func getText(id: ContainerIdLike) -> LoroText { /** * Get a [LoroTree] by container id. * - * If the provided id is string, it will be converted into a root container id with the name of the string. + * If the provided id is string, it will be converted into a root container id with the name of the string. */ open func getTree(id: ContainerIdLike) -> LoroTree { return try! FfiConverterTypeLoroTree.lift(try! rustCall() { @@ -3254,10 +3629,27 @@ open func peerId() -> UInt64 { } /** - * Set the interval of mergeable changes, in seconds. + * Revert the current document state back to the target version + * + * Internally, it will generate a series of local operations that can revert the + * current doc to the target version. It will calculate the diff between the current + * state and the target state, and apply the diff to the current state. + */ +open func revertTo(version: Frontiers)throws {try rustCallWithError(FfiConverterTypeLoroError.lift) { + uniffi_loro_fn_method_lorodoc_revert_to(self.uniffiClonePointer(), + FfiConverterTypeFrontiers.lower(version),$0 + ) +} +} + + /** + * 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. + * + * By default, we record timestamps in seconds for each change. So if the merge interval is 1, and changes A and B + * have timestamps of 3 and 4 respectively, then they will be merged into one change */ open func setChangeMergeInterval(interval: Int64) {try! rustCall() { uniffi_loro_fn_method_lorodoc_set_change_merge_interval(self.uniffiClonePointer(), @@ -3266,6 +3658,11 @@ open func setChangeMergeInterval(interval: Int64) {try! rustCall() { } } + /** + * Set commit message for the current uncommitted changes + * + * It will be persisted. + */ open func setNextCommitMessage(msg: String) {try! rustCall() { uniffi_loro_fn_method_lorodoc_set_next_commit_message(self.uniffiClonePointer(), FfiConverterString.lower(msg),$0 @@ -4931,6 +5328,11 @@ public protocol LoroTextProtocol : AnyObject { */ func getEditorAtUnicodePos(pos: UInt32) -> UInt64? + /** + * Get the text in [Delta](https://quilljs.com/docs/delta/) format. + */ + func getRichtextValue() -> LoroValue + /** * Get the [ContainerID] of the text container. */ @@ -5015,7 +5417,7 @@ public protocol LoroTextProtocol : AnyObject { /** * Get the text in [Delta](https://quilljs.com/docs/delta/) format. */ - func toDelta() -> LoroValue + func toDelta() -> [TextDelta] /** * Get the text content of the text container. @@ -5190,6 +5592,16 @@ open func getEditorAtUnicodePos(pos: UInt32) -> UInt64? { FfiConverterUInt32.lower(pos),$0 ) }) +} + + /** + * Get the text in [Delta](https://quilljs.com/docs/delta/) format. + */ +open func getRichtextValue() -> LoroValue { + return try! FfiConverterTypeLoroValue.lift(try! rustCall() { + uniffi_loro_fn_method_lorotext_get_richtext_value(self.uniffiClonePointer(),$0 + ) +}) } /** @@ -5351,8 +5763,8 @@ open func splice(pos: UInt32, len: UInt32, s: String)throws -> String { /** * Get the text in [Delta](https://quilljs.com/docs/delta/) format. */ -open func toDelta() -> LoroValue { - return try! FfiConverterTypeLoroValue.lift(try! rustCall() { +open func toDelta() -> [TextDelta] { + return try! FfiConverterSequenceTypeTextDelta.lift(try! rustCall() { uniffi_loro_fn_method_lorotext_to_delta(self.uniffiClonePointer(),$0 ) }) @@ -7620,8 +8032,12 @@ public protocol ValueOrContainerProtocol : AnyObject { func asLoroTree() -> LoroTree? + func asLoroUnknown() -> LoroUnknown? + func asValue() -> LoroValue? + func containerType() -> ContainerType? + func isContainer() -> Bool func isValue() -> Bool @@ -7727,6 +8143,13 @@ open func asLoroTree() -> LoroTree? { }) } +open func asLoroUnknown() -> LoroUnknown? { + return try! FfiConverterOptionTypeLoroUnknown.lift(try! rustCall() { + uniffi_loro_fn_method_valueorcontainer_as_loro_unknown(self.uniffiClonePointer(),$0 + ) +}) +} + open func asValue() -> LoroValue? { return try! FfiConverterOptionTypeLoroValue.lift(try! rustCall() { uniffi_loro_fn_method_valueorcontainer_as_value(self.uniffiClonePointer(),$0 @@ -7734,6 +8157,13 @@ open func asValue() -> LoroValue? { }) } +open func containerType() -> ContainerType? { + return try! FfiConverterOptionTypeContainerType.lift(try! rustCall() { + uniffi_loro_fn_method_valueorcontainer_container_type(self.uniffiClonePointer(),$0 + ) +}) +} + open func isContainer() -> Bool { return try! FfiConverterBool.lift(try! rustCall() { uniffi_loro_fn_method_valueorcontainer_is_container(self.uniffiClonePointer(),$0 @@ -8453,6 +8883,54 @@ public func FfiConverterTypeContainerDiff_lower(_ value: ContainerDiff) -> RustB } +public struct ContainerIdAndDiff { + public var cid: ContainerId + public var diff: Diff + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(cid: ContainerId, diff: Diff) { + self.cid = cid + self.diff = diff + } +} + + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public struct FfiConverterTypeContainerIDAndDiff: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ContainerIdAndDiff { + return + try ContainerIdAndDiff( + cid: FfiConverterTypeContainerID.read(from: &buf), + diff: FfiConverterTypeDiff.read(from: &buf) + ) + } + + public static func write(_ value: ContainerIdAndDiff, into buf: inout [UInt8]) { + FfiConverterTypeContainerID.write(value.cid, into: &buf) + FfiConverterTypeDiff.write(value.diff, into: &buf) + } +} + + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeContainerIDAndDiff_lift(_ buf: RustBuffer) throws -> ContainerIdAndDiff { + return try FfiConverterTypeContainerIDAndDiff.lift(buf) +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +public func FfiConverterTypeContainerIDAndDiff_lower(_ value: ContainerIdAndDiff) -> RustBuffer { + return FfiConverterTypeContainerIDAndDiff.lower(value) +} + + public struct ContainerPath { public var id: ContainerId public var path: Index @@ -9719,23 +10197,23 @@ public struct VersionVectorDiff { /** * need to add these spans to move from right to left */ - public var left: [UInt64: CounterSpan] + public var retreat: [UInt64: CounterSpan] /** * need to add these spans to move from left to right */ - public var right: [UInt64: CounterSpan] + public var forward: [UInt64: CounterSpan] // Default memberwise initializers are never public by default, so we // declare one manually. public init( /** * need to add these spans to move from right to left - */left: [UInt64: CounterSpan], + */retreat: [UInt64: CounterSpan], /** * need to add these spans to move from left to right - */right: [UInt64: CounterSpan]) { - self.left = left - self.right = right + */forward: [UInt64: CounterSpan]) { + self.retreat = retreat + self.forward = forward } } @@ -9743,18 +10221,18 @@ public struct VersionVectorDiff { extension VersionVectorDiff: Equatable, Hashable { public static func ==(lhs: VersionVectorDiff, rhs: VersionVectorDiff) -> Bool { - if lhs.left != rhs.left { + if lhs.retreat != rhs.retreat { return false } - if lhs.right != rhs.right { + if lhs.forward != rhs.forward { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(left) - hasher.combine(right) + hasher.combine(retreat) + hasher.combine(forward) } } @@ -9766,14 +10244,14 @@ public struct FfiConverterTypeVersionVectorDiff: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> VersionVectorDiff { return try VersionVectorDiff( - left: FfiConverterDictionaryUInt64TypeCounterSpan.read(from: &buf), - right: FfiConverterDictionaryUInt64TypeCounterSpan.read(from: &buf) + retreat: FfiConverterDictionaryUInt64TypeCounterSpan.read(from: &buf), + forward: FfiConverterDictionaryUInt64TypeCounterSpan.read(from: &buf) ) } public static func write(_ value: VersionVectorDiff, into buf: inout [UInt8]) { - FfiConverterDictionaryUInt64TypeCounterSpan.write(value.left, into: &buf) - FfiConverterDictionaryUInt64TypeCounterSpan.write(value.right, into: &buf) + FfiConverterDictionaryUInt64TypeCounterSpan.write(value.retreat, into: &buf) + FfiConverterDictionaryUInt64TypeCounterSpan.write(value.forward, into: &buf) } } @@ -10746,6 +11224,8 @@ public enum LoroError { case InvalidPeerId(message: String) + case ContainersNotFound(message: String) + } @@ -10906,6 +11386,10 @@ public struct FfiConverterTypeLoroError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) + case 37: return .ContainersNotFound( + message: try FfiConverterString.read(from: &buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase } @@ -10989,6 +11473,8 @@ public struct FfiConverterTypeLoroError: FfiConverterRustBuffer { writeInt(&buf, Int32(35)) case .InvalidPeerId(_ /* message is ignored*/): writeInt(&buf, Int32(36)) + case .ContainersNotFound(_ /* message is ignored*/): + writeInt(&buf, Int32(37)) } @@ -11988,6 +12474,30 @@ fileprivate struct FfiConverterOptionTypeLoroTree: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeLoroUnknown: FfiConverterRustBuffer { + typealias SwiftType = LoroUnknown? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeLoroUnknown.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeLoroUnknown.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -12228,6 +12738,54 @@ fileprivate struct FfiConverterOptionTypeContainerID: FfiConverterRustBuffer { } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeContainerType: FfiConverterRustBuffer { + typealias SwiftType = ContainerType? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeContainerType.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeContainerType.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterOptionTypeDiff: FfiConverterRustBuffer { + typealias SwiftType = Diff? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeDiff.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeDiff.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -12497,6 +13055,31 @@ fileprivate struct FfiConverterSequenceTypeContainerDiff: FfiConverterRustBuffer } } +#if swift(>=5.8) +@_documentation(visibility: private) +#endif +fileprivate struct FfiConverterSequenceTypeContainerIDAndDiff: FfiConverterRustBuffer { + typealias SwiftType = [ContainerIdAndDiff] + + public static func write(_ value: [ContainerIdAndDiff], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeContainerIDAndDiff.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [ContainerIdAndDiff] { + let len: Int32 = try readInt(&buf) + var seq = [ContainerIdAndDiff]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeContainerIDAndDiff.read(from: &buf)) + } + return seq + } +} + #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -12978,6 +13561,12 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_containeridlike_as_container_id() != 41081) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_diffbatch_get_diff() != 42707) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_loro_checksum_method_diffbatch_push() != 56678) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_fractionalindex_to_string() != 57024) { return InitializationResult.apiChecksumMismatch } @@ -13005,6 +13594,9 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorocounter_is_deleted() != 12079) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorodoc_apply_diff() != 45393) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorodoc_attach() != 7252) { return InitializationResult.apiChecksumMismatch } @@ -13038,9 +13630,18 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorodoc_detach() != 61399) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorodoc_diff() != 38416) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_loro_checksum_method_lorodoc_export_json_in_id_span() != 26608) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorodoc_export_json_updates() != 15152) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorodoc_export_json_updates_without_peer_compression() != 23184) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorodoc_export_shallow_snapshot() != 27927) { return InitializationResult.apiChecksumMismatch } @@ -13059,6 +13660,9 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorodoc_export_updates_in_range() != 22491) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorodoc_find_id_spans_between() != 1313) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorodoc_fork() != 45665) { return InitializationResult.apiChecksumMismatch } @@ -13167,6 +13771,9 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorodoc_peer_id() != 35449) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorodoc_revert_to() != 48346) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorodoc_set_change_merge_interval() != 55133) { return InitializationResult.apiChecksumMismatch } @@ -13440,6 +14047,9 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorotext_get_editor_at_unicode_pos() != 24596) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_lorotext_get_richtext_value() != 45999) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_lorotext_id() != 30925) { return InitializationResult.apiChecksumMismatch } @@ -13479,7 +14089,7 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_lorotext_splice() != 30467) { return InitializationResult.apiChecksumMismatch } - if (uniffi_loro_checksum_method_lorotext_to_delta() != 57631) { + if (uniffi_loro_checksum_method_lorotext_to_delta() != 15868) { return InitializationResult.apiChecksumMismatch } if (uniffi_loro_checksum_method_lorotext_to_string() != 63765) { @@ -13647,9 +14257,15 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_method_valueorcontainer_as_loro_tree() != 39545) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_valueorcontainer_as_loro_unknown() != 9911) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_valueorcontainer_as_value() != 9638) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_method_valueorcontainer_container_type() != 56498) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_method_valueorcontainer_is_container() != 16329) { return InitializationResult.apiChecksumMismatch } @@ -13701,6 +14317,9 @@ private var initializationResult: InitializationResult = { if (uniffi_loro_checksum_constructor_cursor_new() != 11721) { return InitializationResult.apiChecksumMismatch } + if (uniffi_loro_checksum_constructor_diffbatch_new() != 62583) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_loro_checksum_constructor_fractionalindex_from_bytes() != 35415) { return InitializationResult.apiChecksumMismatch } diff --git a/loro-rs/Cargo.lock b/loro-rs/Cargo.lock index 284dbd8..949f779 100644 --- a/loro-rs/Cargo.lock +++ b/loro-rs/Cargo.lock @@ -620,7 +620,7 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loro" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "enum-as-inner 0.6.1", "fxhash", @@ -635,7 +635,7 @@ dependencies = [ [[package]] name = "loro-common" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "arbitrary", "enum-as-inner 0.6.1", @@ -652,7 +652,7 @@ dependencies = [ [[package]] name = "loro-delta" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "arrayvec", "enum-as-inner 0.5.1", @@ -663,7 +663,7 @@ dependencies = [ [[package]] name = "loro-ffi" version = "1.1.3" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "loro", "loro-internal", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "loro-internal" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "append-only-bytes", "arref", @@ -713,7 +713,7 @@ dependencies = [ [[package]] name = "loro-kv-store" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "bytes", "ensure-cov", @@ -729,7 +729,7 @@ dependencies = [ [[package]] name = "loro-rle" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "append-only-bytes", "num", @@ -738,7 +738,7 @@ dependencies = [ [[package]] name = "loro-swift" -version = "1.2.7-alpha" +version = "1.3.0" dependencies = [ "loro-ffi", "uniffi", @@ -753,7 +753,7 @@ checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" [[package]] name = "loro_fractional_index" version = "1.2.7" -source = "git+https://github.com/loro-dev/loro.git?branch=main#ca51a78bacc1894c49bd34c52f5bef7de622d8f5" +source = "git+https://github.com/loro-dev/loro.git?tag=loro-ffi%401.3.0#2df24725df3e9ed51f1dd2501730cc3785b46d4b" dependencies = [ "once_cell", "rand", diff --git a/loro-rs/Cargo.toml b/loro-rs/Cargo.toml index c25a866..859824d 100644 --- a/loro-rs/Cargo.toml +++ b/loro-rs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loro-swift" -version = "1.2.7-alpha" +version = "1.3.0" edition = "2021" [lib] @@ -14,8 +14,8 @@ path = "src/uniffi-bindgen.rs" [dependencies] +loro-ffi = { git = "https://github.com/loro-dev/loro.git", tag = "loro-ffi@1.3.0" } # loro-ffi = { path = "../../loro/crates/loro-ffi" } -loro-ffi = { git = "https://github.com/loro-dev/loro.git", branch = "main" } uniffi = { version = "0.28.3" } [build-dependencies] diff --git a/loro-rs/src/loro.udl b/loro-rs/src/loro.udl index 9629c71..545b6f7 100644 --- a/loro-rs/src/loro.udl +++ b/loro-rs/src/loro.udl @@ -10,6 +10,7 @@ namespace loro{ interface ValueOrContainer{ boolean is_value(); boolean is_container(); + ContainerType? container_type(); LoroValue? as_value(); ContainerID? as_container(); LoroText? as_loro_text(); @@ -18,6 +19,7 @@ interface ValueOrContainer{ LoroTree? as_loro_tree(); LoroCounter? as_loro_counter(); LoroMovableList? as_loro_movable_list(); + LoroUnknown? as_loro_unknown(); }; [Trait, WithForeign] @@ -114,10 +116,13 @@ interface LoroDoc{ /// the largest existing timestamp will be used instead. void set_record_timestamp(boolean record); - /// Set the interval of mergeable changes, in seconds. + /// 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. + /// + /// By default, we record timestamps in seconds for each change. So if the merge interval is 1, and changes A and B + /// have timestamps of 3 and 4 respectively, then they will be merged into one change void set_change_merge_interval(i64 interval); /// Set the rich text format configuration of the document. @@ -222,6 +227,9 @@ interface LoroDoc{ void commit_with(CommitOptions options); + /// Set commit message for the current uncommitted changes + /// + /// It will be persisted. void set_next_commit_message([ByRef] string msg); /// Whether the document is in detached mode, where the [loro_internal::DocState] is not @@ -338,8 +346,58 @@ interface LoroDoc{ /// Get the handler by the path. ValueOrContainer? get_by_path([ByRef] sequence path); - - /// Get the handler by the string path. + + /// The path can be specified in different ways depending on the container type: + /// + /// For Tree: + /// 1. Using node IDs: `tree/{node_id}/property` + /// 2. Using indices: `tree/0/1/property` + /// + /// For List and MovableList: + /// - Using indices: `list/0` or `list/1/property` + /// + /// For Map: + /// - Using keys: `map/key` or `map/nested/property` + /// + /// For tree structures, index-based paths follow depth-first traversal order. + /// The indices start from 0 and represent the position of a node among its siblings. + /// + /// # Examples + /// ``` + /// # use loro::{LoroDoc, LoroValue}; + /// let doc = LoroDoc::new(); + /// + /// // Tree example + /// let tree = doc.get_tree("tree"); + /// let root = tree.create(None).unwrap(); + /// tree.get_meta(root).unwrap().insert("name", "root").unwrap(); + /// // Access tree by ID or index + /// let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap(); + /// let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap(); + /// assert_eq!(name1, name2); + /// + /// // List example + /// let list = doc.get_list("list"); + /// list.insert(0, "first").unwrap(); + /// list.insert(1, "second").unwrap(); + /// // Access list by index + /// let item = doc.get_by_str_path("list/0"); + /// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into()); + /// + /// // Map example + /// let map = doc.get_map("map"); + /// map.insert("key", "value").unwrap(); + /// // Access map by key + /// let value = doc.get_by_str_path("map/key"); + /// assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into()); + /// + /// // MovableList example + /// let mlist = doc.get_movable_list("mlist"); + /// mlist.insert(0, "item").unwrap(); + /// // Access movable list by index + /// let item = doc.get_by_str_path("mlist/0"); + /// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into()); + /// ``` ValueOrContainer? get_by_str_path([ByRef] string path); [Throws=CannotFindRelativePosition] @@ -449,6 +507,36 @@ interface LoroDoc{ /// The pending transaction is the one that is not committed yet. It will be committed /// after calling `doc.commit()`, `doc.export(mode)` or `doc.checkout(version)`. u32 get_pending_txn_len(); + + /// Export the current state with json-string format of the document, without peer compression. + /// + /// Compared to [`export_json_updates`], this method does not compress the peer IDs in the updates. + /// So the operations are easier to be processed by application code. + string export_json_updates_without_peer_compression([ByRef] VersionVector start_vv, [ByRef] VersionVector end_vv); + + /// Export the readable [`Change`]s in the given [`IdSpan`] + sequence export_json_in_id_span(IdSpan id_span); + + /// Find the operation id spans that between the `from` version and the `to` version. + VersionVectorDiff find_id_spans_between([ByRef] Frontiers from, [ByRef] Frontiers to); + + /// Revert the current document state back to the target version + /// + /// Internally, it will generate a series of local operations that can revert the + /// current doc to the target version. It will calculate the diff between the current + /// state and the target state, and apply the diff to the current state. + [Throws=LoroError] + void revert_to([ByRef] Frontiers version); + + /// Apply a diff to the current document state. + /// + /// Internally, it will apply the diff to the current state. + [Throws=LoroError] + void apply_diff([ByRef] DiffBatch diff); + + /// Calculate the diff between two versions + [Throws=LoroError] + DiffBatch diff([ByRef] Frontiers a, [ByRef] Frontiers b); }; dictionary ContainerPath{ @@ -1218,9 +1306,9 @@ interface Frontiers{ dictionary VersionVectorDiff{ /// need to add these spans to move from right to left - record left; + record retreat; /// need to add these spans to move from left to right - record right; + record forward; }; // ============= UNDO MANAGER ============= @@ -1397,6 +1485,29 @@ interface Subscription{ void detach(); }; +interface DiffBatch{ + constructor(); + + /// Push a new event to the batch. + /// + /// If the cid already exists in the batch, return Err + Diff? push(ContainerID cid, Diff diff); + + /// Returns an iterator over the diffs in this batch, in the order they were added. + /// + /// The iterator yields tuples of `(&ContainerID, &Diff)` where: + /// - `ContainerID` is the ID of the container that was modified + /// - `Diff` contains the actual changes made to that container + /// + /// The order of the diffs is preserved from when they were originally added to the batch. + sequence get_diff(); +}; + +dictionary ContainerIDAndDiff{ + ContainerID cid; + Diff diff; +}; + // ============= TYPES ============= dictionary TreeID{ u64 peer;