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

Expose heads raw state representation #191

Merged
merged 4 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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)
}
}
Loading