Skip to content

Commit

Permalink
Expose heads raw state representation (#191)
Browse files Browse the repository at this point in the history
* expose heads raw that represent an arbitrary document's state

minor

* making heads raw encode into opaque Data type

* preserve sort integrity between executions to calculate heads raw

minor

* Update Sources/Automerge/ChangeHash.swift

Co-authored-by: Joseph Heck <[email protected]>

---------

Co-authored-by: Joseph Heck <[email protected]>
  • Loading branch information
miguelangel-dev and heckj authored Jul 13, 2024
1 parent 354f681 commit 90f301a
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 0 deletions.
31 changes: 31 additions & 0 deletions Sources/Automerge/ChangeHash.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AutomergeUniffi
import Foundation

/// An opaque hash that represents a change within an Automerge document.
public struct ChangeHash: Equatable, Hashable, CustomDebugStringConvertible, Sendable {
Expand All @@ -9,3 +10,33 @@ public struct ChangeHash: Equatable, Hashable, CustomDebugStringConvertible, Sen
bytes.map { String(format: "%02hhx", $0) }.joined()
}
}

public extension Set<ChangeHash> {

/// Transforms each `ChangeHash` in the set into its byte array (`[UInt8]`). This raw byte representation
/// captures the state of the document at a specific point in its history, allowing for efficient storage
/// and retrieval of document states.
func raw() -> Data {
let rawBytes = map(\.bytes).sorted { lhs, rhs in
lhs.debugDescription > rhs.debugDescription
}
return Data(rawBytes.joined())
}
}

public extension Data {

/// Interprets the data to return the data as a set of change hashes that represent a state within an Automerge document. If the data is not a multiple of 32 bytes, returns nil.
func heads() -> Set<ChangeHash>? {
let rawBytes: [UInt8] = Array(self)
guard rawBytes.count % 32 == 0 else { return nil }
let totalHashes = rawBytes.count / 32
let heads = (0..<totalHashes).map { index in
let lowerBound = index * 32
let upperBound = (index + 1) * 32
let bytes = rawBytes[lowerBound..<upperBound]
return ChangeHash(bytes: Array(bytes))
}
return Set(heads)
}
}
33 changes: 33 additions & 0 deletions Tests/AutomergeTests/TestChanges.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,37 @@ class ChangeSetTests: XCTestCase {
XCTAssertEqual(patches1.count, 1)
XCTAssertEqual(patches1, patches2)
}

func testRelationBetweenChangeHashAndRaw() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
let doc1 = doc.fork()
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "Hello")
try doc1.spliceText(obj: textId, start: 0, delete: 0, value: " World!")
try doc.merge(other: doc1)

let heads = doc.heads()
let restored = doc.heads().raw().heads()

XCTAssertEqual(heads, restored)
}

func testChangeHash_SameHeads_ResultSameRawData() throws {
let doc = Document()
let textId = try! doc.putObject(obj: ObjId.ROOT, key: "text", ty: .Text)
let doc1 = doc.fork()
let doc2 = doc.fork()
let doc3 = doc.fork()
try doc.spliceText(obj: textId, start: 0, delete: 0, value: "[0]")
try doc1.spliceText(obj: textId, start: 0, delete: 0, value: "[1]")
try doc2.spliceText(obj: textId, start: 0, delete: 0, value: "[2]")
try doc3.spliceText(obj: textId, start: 0, delete: 0, value: "[3]")
try doc.merge(other: doc1)
try doc.merge(other: doc2)
try doc.merge(other: doc3)

let rawHashes = (0..<500).map { _ in doc.heads().raw() }

XCTAssertEqual(Set(rawHashes).count, 1)
}
}

0 comments on commit 90f301a

Please sign in to comment.