Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added commitWith() method to specify message/timestamp and exposing changes via changeByHash() #129

Merged
merged 5 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions AutomergeUniffi/automerge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,10 @@ public protocol DocProtocol: AnyObject {

func changes() -> [ChangeHash]

func changeByHash(hash: ChangeHash) -> Change?

func commitWith(msg: String?, time: Int64)

func cursor(obj: ObjId, position: UInt64) throws -> Cursor

func cursorAt(obj: ObjId, position: UInt64, heads: [ChangeHash]) throws -> Cursor
Expand Down Expand Up @@ -521,6 +525,7 @@ public protocol DocProtocol: AnyObject {
func values(obj: ObjId) throws -> [Value]

func valuesAt(obj: ObjId, heads: [ChangeHash]) throws -> [Value]

}

public class Doc:
Expand Down Expand Up @@ -614,6 +619,20 @@ public class Doc:
)
}

public func changeByHash(hash: ChangeHash) -> Change? {
try! FfiConverterOptionTypeChange.lift(
try!
rustCall {
uniffi_uniffi_automerge_fn_method_doc_change_by_hash(
self.uniffiClonePointer(),

FfiConverterTypeChangeHash.lower(hash),
$0
)
}
)
}

public func cursor(obj: ObjId, position: UInt64) throws -> Cursor {
try FfiConverterTypeCursor.lift(
rustCallWithError(FfiConverterTypeDocError.lift) {
Expand Down Expand Up @@ -1219,6 +1238,18 @@ public class Doc:
}
)
}
public func commitWith(msg: String?, time: Int64) {
try!
rustCall {
uniffi_uniffi_automerge_fn_method_doc_commit_with(
self.uniffiClonePointer(),

FfiConverterOptionString.lower(msg),
FfiConverterInt64.lower(time),
$0
)
}
}

public func save() -> [UInt8] {
try! FfiConverterSequenceUInt8.lift(
Expand Down Expand Up @@ -1495,6 +1526,96 @@ public func FfiConverterTypeSyncState_lower(_ value: SyncState) -> UnsafeMutable
FfiConverterTypeSyncState.lower(value)
}

public struct Change {
public var actorId: ActorId
public var message: String?
public var deps: [ChangeHash]
public var timestamp: Int64
public var bytes: [UInt8]
bgomberg marked this conversation as resolved.
Show resolved Hide resolved
public var hash: ChangeHash

// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(
alexjg marked this conversation as resolved.
Show resolved Hide resolved
actorId: ActorId,
message: String?,
deps: [ChangeHash],
timestamp: Int64,
bytes: [UInt8],
hash: ChangeHash
) {
self.actorId = actorId
self.message = message
self.deps = deps
self.timestamp = timestamp
self.bytes = bytes
self.hash = hash
}
}

extension Change: Equatable, Hashable {
public static func == (lhs: Change, rhs: Change) -> Bool {
if lhs.actorId != rhs.actorId {
return false
}
if lhs.message != rhs.message {
return false
}
if lhs.deps != rhs.deps {
return false
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.bytes != rhs.bytes {
return false
}
if lhs.hash != rhs.hash {
return false
}
return true
}

public func hash(into hasher: inout Hasher) {
hasher.combine(actorId)
hasher.combine(message)
hasher.combine(deps)
hasher.combine(timestamp)
hasher.combine(bytes)
hasher.combine(hash)
}
}

public struct FfiConverterTypeChange: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Change {
try Change(
actorId: FfiConverterTypeActorId.read(from: &buf),
message: FfiConverterOptionString.read(from: &buf),
deps: FfiConverterSequenceTypeChangeHash.read(from: &buf),
timestamp: FfiConverterInt64.read(from: &buf),
bytes: FfiConverterSequenceUInt8.read(from: &buf),
hash: FfiConverterTypeChangeHash.read(from: &buf)
)
}

public static func write(_ value: Change, into buf: inout [UInt8]) {
FfiConverterTypeActorId.write(value.actorId, into: &buf)
FfiConverterOptionString.write(value.message, into: &buf)
FfiConverterSequenceTypeChangeHash.write(value.deps, into: &buf)
FfiConverterInt64.write(value.timestamp, into: &buf)
FfiConverterSequenceUInt8.write(value.bytes, into: &buf)
FfiConverterTypeChangeHash.write(value.hash, into: &buf)
}
}

public func FfiConverterTypeChange_lift(_ buf: RustBuffer) throws -> Change {
try FfiConverterTypeChange.lift(buf)
}

public func FfiConverterTypeChange_lower(_ value: Change) -> RustBuffer {
FfiConverterTypeChange.lower(value)
}

public struct KeyValue {
public var key: String
public var value: Value
Expand Down Expand Up @@ -2391,6 +2512,48 @@ public func FfiConverterTypeValue_lower(_ value: Value) -> RustBuffer {

extension Value: Equatable, Hashable {}

private struct FfiConverterOptionString: FfiConverterRustBuffer {
typealias SwiftType = String?

public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else {
writeInt(&buf, Int8(0))
return
}
writeInt(&buf, Int8(1))
FfiConverterString.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 FfiConverterString.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag
}
}
}

private struct FfiConverterOptionTypeChange: FfiConverterRustBuffer {
typealias SwiftType = Change?

public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else {
writeInt(&buf, Int8(0))
return
}
writeInt(&buf, Int8(1))
FfiConverterTypeChange.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 FfiConverterTypeChange.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag
}
}
}

private struct FfiConverterOptionTypeValue: FfiConverterRustBuffer {
typealias SwiftType = Value?

Expand Down Expand Up @@ -2835,9 +2998,15 @@ private var initializationResult: InitializationResult {
if uniffi_uniffi_automerge_checksum_method_doc_apply_encoded_changes_with_patches() != 63928 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_uniffi_automerge_checksum_method_doc_change_by_hash() != 44577 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_uniffi_automerge_checksum_method_doc_changes() != 1878 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_uniffi_automerge_checksum_method_doc_commit_with() != 65319 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_uniffi_automerge_checksum_method_doc_cursor() != 18441 {
return InitializationResult.apiChecksumMismatch
}
Expand Down
22 changes: 22 additions & 0 deletions Sources/Automerge/Change.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import struct AutomergeUniffi.Change
import Foundation

typealias FfiChange = AutomergeUniffi.Change

public struct Change: Equatable {
public let actorId: ActorId
public let message: String?
public let deps: [ChangeHash]
public let timestamp: Date
public let bytes: Data
public let hash: ChangeHash

init(_ ffi: FfiChange) {
actorId = ActorId(bytes: ffi.actorId)
message = ffi.message
deps = ffi.deps.map(ChangeHash.init(bytes:))
timestamp = Date(timeIntervalSince1970: TimeInterval(ffi.timestamp))
bytes = Data(ffi.bytes)
hash = ChangeHash(bytes: ffi.hash)
}
}
27 changes: 27 additions & 0 deletions Sources/Automerge/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,23 @@ public final class Document: @unchecked Sendable {
}
}

/// Commit the auto-generated transaction with options.
///
/// - Parameters:
/// - message: An optional message to attach to the auto-committed change (if any).
/// - timestamp: An optional timestamp to attach to the auto-committed change (if any).
///
/// The `commitWith` function also compacts the memory footprint of an Automerge document and increments the
bgomberg marked this conversation as resolved.
Show resolved Hide resolved
/// result of ``heads()``, which indicates a specific point in time for the history of the document.
public func commitWith(message: String? = nil, timestamp: Date = Date()) {
sync {
self.doc.wrapErrors {
sendObjectWillChange()
$0.commitWith(msg: message, time: Int64(timestamp.timeIntervalSince1970))
}
}
}

/// Encode the Automerge document in a compressed binary format.
bgomberg marked this conversation as resolved.
Show resolved Hide resolved
///
/// - Returns: The data that represents all the changes within this document.
Expand Down Expand Up @@ -922,6 +939,16 @@ public final class Document: @unchecked Sendable {
}
}

/// Returns the contents of the change associated with the change hash you provide.
public func change(hash: ChangeHash) -> Change? {
sync {
guard let change = self.doc.wrapErrors(f: { $0.changeByHash(hash: hash.bytes) }) else {
return nil
}
return .init(change)
}
}

/// Get the path to an object within the document.
///
/// - Parameter obj: The identifier of an array, dictionary or text object.
Expand Down
10 changes: 10 additions & 0 deletions Sources/_CAutomergeUniffi/include/automergeFFI.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ void uniffi_uniffi_automerge_fn_method_doc_apply_encoded_changes(void*_Nonnull p
);
RustBuffer uniffi_uniffi_automerge_fn_method_doc_apply_encoded_changes_with_patches(void*_Nonnull ptr, RustBuffer changes, RustCallStatus *_Nonnull out_status
);
RustBuffer uniffi_uniffi_automerge_fn_method_doc_change_by_hash(void*_Nonnull ptr, RustBuffer hash, RustCallStatus *_Nonnull out_status
);
RustBuffer uniffi_uniffi_automerge_fn_method_doc_changes(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
);
void uniffi_uniffi_automerge_fn_method_doc_commit_with(void*_Nonnull ptr, RustBuffer msg, int64_t time, RustCallStatus *_Nonnull out_status
);
RustBuffer uniffi_uniffi_automerge_fn_method_doc_cursor(void*_Nonnull ptr, RustBuffer obj, uint64_t position, RustCallStatus *_Nonnull out_status
);
RustBuffer uniffi_uniffi_automerge_fn_method_doc_cursor_at(void*_Nonnull ptr, RustBuffer obj, uint64_t position, RustBuffer heads, RustCallStatus *_Nonnull out_status
Expand Down Expand Up @@ -315,9 +319,15 @@ uint16_t uniffi_uniffi_automerge_checksum_method_doc_apply_encoded_changes(void
);
uint16_t uniffi_uniffi_automerge_checksum_method_doc_apply_encoded_changes_with_patches(void

);
uint16_t uniffi_uniffi_automerge_checksum_method_doc_change_by_hash(void

);
uint16_t uniffi_uniffi_automerge_checksum_method_doc_changes(void

);
uint16_t uniffi_uniffi_automerge_checksum_method_doc_commit_with(void

);
uint16_t uniffi_uniffi_automerge_checksum_method_doc_cursor(void

Expand Down
54 changes: 54 additions & 0 deletions Tests/AutomergeTests/DocTests/AutomergeDocTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,58 @@ final class AutomergeDocTests: XCTestCase {
let text = try doc.text(obj: textId)
XCTAssertEqual(text, "🇬🇧😀")
}

func testCommitWith() throws {
struct Dog: Codable {
var name: String
var age: Int
}

// Create the document
let doc = Document()
let encoder = AutomergeEncoder(doc: doc)

// Make an initial change with a message and timestamp
var myDog = Dog(name: "Fido", age: 1)
try encoder.encode(myDog)
doc.commitWith(message: "Change 1", timestamp: Date(timeIntervalSince1970: 10))

// Make another change with the default timestamp
myDog.age = 2
try encoder.encode(myDog)
doc.commitWith(message: "Change 2")
let change2Time = Date().timeIntervalSince1970

// Make another change with no message
myDog.age = 3
try encoder.encode(myDog)
doc.commitWith(message: nil, timestamp: Date(timeIntervalSince1970: 20))

// Make another change with no message and the default timestamp
myDog.age = 4
try encoder.encode(myDog)
doc.commitWith()
let change4Time = Date().timeIntervalSince1970

// Make another change by just calling save() (meaning no commit options will be set)
myDog.age = 5
try encoder.encode(myDog)
_ = doc.save()

let history = doc.getHistory()
XCTAssertEqual(history.count, 5)

let changes = history.map({ doc.change(hash: $0) })
XCTAssertEqual(changes.count, 5)
XCTAssertEqual(changes[0]!.message, "Change 1")
XCTAssertEqual(changes[0]!.timestamp, Date(timeIntervalSince1970: 10))
XCTAssertEqual(changes[1]!.message, "Change 2")
XCTAssertEqual(changes[1]!.timestamp.timeIntervalSince1970, change2Time, accuracy: 3)
XCTAssertNil(changes[2]!.message)
XCTAssertEqual(changes[2]!.timestamp, Date(timeIntervalSince1970: 20))
XCTAssertNil(changes[3]!.message)
XCTAssertEqual(changes[3]!.timestamp.timeIntervalSince1970, change4Time, accuracy: 3)
XCTAssertNil(changes[4]!.message)
XCTAssertEqual(changes[4]!.timestamp.timeIntervalSince1970, 0)
}
}
13 changes: 13 additions & 0 deletions rust/src/automerge.udl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ dictionary PathElement {
ObjId obj;
};

dictionary Change {
ActorId actor_id;
string? message;
sequence<ChangeHash> deps;
i64 timestamp;
sequence<u8> bytes;
ChangeHash hash;
};

dictionary Patch {
sequence<PathElement> path;
PatchAction action;
Expand Down Expand Up @@ -222,6 +231,10 @@ interface Doc {

sequence<ChangeHash> changes();

Change? change_by_hash(ChangeHash hash);

void commit_with(string? msg, i64 time);

sequence<u8> save();

[Throws=DocError]
Expand Down
Loading
Loading