Skip to content

Commit

Permalink
iterating on repo and return types
Browse files Browse the repository at this point in the history
  • Loading branch information
heckj committed Mar 22, 2024
1 parent 8232aa8 commit 1ebe119
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct DocHandle: Sendable {
*/

let id: DocumentId
weak var _doc: Automerge.Document?
var _doc: Automerge.Document?
var state: DocHandleState
var remoteHeads: [STORAGE_ID: Set<Automerge.ChangeHash>]
var syncStates: [PEER_ID: SyncState]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public actor NetworkSubsystem {
// Save the throwing scenarios for failures in connection, etc.
guard let repo else {
// invariant that there should be a valid doc handle available from the repo
fatalError("DocHandle isn't available from the repo")
throw Errors.Unavailable(id: id)
}

let newDocument = Document()
Expand Down
79 changes: 50 additions & 29 deletions Packages/automerge-repo/Sources/AutomergeRepo/Repo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public protocol EphemeralMessageDelegate: Sendable {

public actor Repo {
public let peerId: PEER_ID
public let localPeerMetadata: PeerMetadata
public var localPeerMetadata: PeerMetadata
// to replace DocumentSyncCoordinator
private var handles: [DocumentId: DocHandle] = [:]
private var storage: DocumentStorage?
Expand Down Expand Up @@ -78,29 +78,29 @@ public actor Repo {
// - func subscribeToRemotes([StorageId])

init(
storageProvider: (some StorageProvider)? = nil,
networkAdapters: [any NetworkProvider] = [],
sharePolicy: some SharePolicy
) async {
) {
self.peerId = UUID().uuidString
self.handles = [:]
self.peerMetadataByPeerId = [:]
self.localPeerMetadata = await PeerMetadata(storageId: storage?.id, isEphemeral: storageProvider == nil)
if let provider = storageProvider {
self.storage = DocumentStorage(provider)
} else {
self.storage = nil
}
self.storage = nil
self.localPeerMetadata = PeerMetadata(storageId: nil, isEphemeral: true)
self.sharePolicy = sharePolicy
self.network = NetworkSubsystem()
// ALL STORED PROPERTIES ARE SET BY HERE
}

// TODO: load up any persistent data from the storage...
public func addStorageProvider(_ provider: some StorageProvider) {
self.storage = DocumentStorage(provider)
self.localPeerMetadata = PeerMetadata(storageId: provider.id, isEphemeral: false)
}

await self.network.setRepo(self)
for adapter in networkAdapters {
await network.addAdapter(adapter: adapter)
/// Add a configured network provider to the repo
/// - Parameter adapter: <#adapter description#>
public func addNetworkAdapter(adapter: any NetworkProvider) async {
if await self.network.repo == nil {
await self.network.setRepo(self)
}
await network.addAdapter(adapter: adapter)
}

public func setDelegate(_ delegate: some EphemeralMessageDelegate) {
Expand Down Expand Up @@ -254,40 +254,56 @@ public actor Repo {

/// Creates a new Automerge document, storing it and sharing the creation with connected peers.
/// - Returns: The Automerge document.
public func create() async throws -> Document {
public func create() async throws -> (DocumentId, Document) {
let handle = DocHandle(id: DocumentId(), isNew: true, initialValue: Document())
self.handles[handle.id] = handle
return try await resolveDocHandle(id: handle.id)
let resolved = try await resolveDocHandle(id: handle.id)
return (handle.id, resolved)
}

/// Creates a new Automerge document, storing it and sharing the creation with connected peers.
/// - Returns: The Automerge document.
/// - Parameter id: The Id of the Automerge document.
public func create(id: DocumentId) async throws -> (DocumentId, Document) {
let handle = DocHandle(id: id, isNew: true, initialValue: Document())
self.handles[handle.id] = handle
let resolved = try await resolveDocHandle(id: handle.id)
return (id, resolved)
}

/// Creates a new Automerge document, storing it and sharing the creation with connected peers.
/// - Parameter doc: The Automerge document to use for the new, shared document
/// - Returns: The Automerge document.
public func create(doc: Document) async throws -> Document {
let handle = DocHandle(id: DocumentId(), isNew: true, initialValue: doc)
public func create(doc: Document, id: DocumentId? = nil) async throws -> (DocumentId, Document) {
let creationId = id ?? DocumentId()
let handle = DocHandle(id: creationId, isNew: true, initialValue: doc)
self.handles[handle.id] = handle
return try await resolveDocHandle(id: handle.id)
let resolved = try await resolveDocHandle(id: handle.id)
return (handle.id, resolved)
}

/// Creates a new Automerge document, storing it and sharing the creation with connected peers.
/// - Parameter data: The data to load as an Automerge document for the new, shared document.
/// - Returns: The Automerge document.
public func create(data: Data) async throws -> Document {
let handle = try DocHandle(id: DocumentId(), isNew: true, initialValue: Document(data))
public func create(data: Data, id: DocumentId? = nil) async throws -> (DocumentId, Document) {
let creationId = id ?? DocumentId()
let handle = try DocHandle(id: creationId, isNew: true, initialValue: Document(data))
self.handles[handle.id] = handle
return try await resolveDocHandle(id: handle.id)
let resolved = try await resolveDocHandle(id: handle.id)
return (handle.id, resolved)
}

/// Clones a document the repo already knows to create a new, shared document.
/// - Parameter id: The id of the document to clone.
/// - Returns: The Automerge document.
public func clone(id: DocumentId) async throws -> Document {
public func clone(id: DocumentId) async throws -> (DocumentId, Document) {
let originalDoc = try await resolveDocHandle(id: id)
let fork = originalDoc.fork()
let newId = DocumentId()
let newHandle = DocHandle(id: newId, isNew: false, initialValue: fork)
handles[newId] = newHandle
return try await resolveDocHandle(id: newId)
handles[newHandle.id] = newHandle
let resolved = try await resolveDocHandle(id: newHandle.id)
return (newHandle.id, resolved)
}

public func find(id: DocumentId) async throws -> Document {
Expand Down Expand Up @@ -345,7 +361,10 @@ public actor Repo {
/// The storage id of this repo, if any.
/// - Returns: The storage id from the repo's storage provider or nil.
public func storageId() async -> STORAGE_ID? {
await storage?.id
if let storage {
return await storage.id
}
return nil
}

// MARK: Methods to expose retrieving DocHandles to the subsystems
Expand Down Expand Up @@ -459,8 +478,10 @@ public actor Repo {
if let docFromHandle = handle._doc {
// We have the document - so being in loading means "try to save this to
// a storage provider, if one exists", then hand it back as good.
Task.detached {
try await self.storage?.saveDoc(id: id, doc: docFromHandle)
if let storage = self.storage {
Task.detached {
try await storage.saveDoc(id: id, doc: docFromHandle)
}
}
// TODO: if we're allowed and prolific in gossip, notify any connected
// peers there's a new document before jumping to the 'ready' state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class DocHandleTests: XCTestCase {
XCTAssertEqual(new.isReady, false)
XCTAssertEqual(new.isUnavailable, false)
XCTAssertEqual(new.remoteHeads.count, 0)
XCTAssertNil(new._doc)
XCTAssertNotNil(new._doc)
}

func testDocHandleRequestData() async throws {
Expand All @@ -52,6 +52,6 @@ final class DocHandleTests: XCTestCase {
XCTAssertEqual(new.isReady, true)
XCTAssertEqual(new.isUnavailable, false)
XCTAssertEqual(new.remoteHeads.count, 0)
XCTAssertNil(new._doc)
XCTAssertNotNil(new._doc)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,12 @@ public final class InMemoryNetworkEndpoint: NetworkProvider {
// MARK: TESTING SPECIFIC API

public func createNetworkEndpoint(
named: String,
config: InMemoryNetworkEndpoint.BasicNetworkConfiguration
) async {
) async -> InMemoryNetworkEndpoint {
let x = await InMemoryNetworkEndpoint()
endpoints[named] = x
endpoints[config.name] = x
await x.configure(config)
return x
}

public func connect(from: String, to: String, latency: Duration?) async throws -> InMemoryNetworkConnection {
Expand Down
135 changes: 135 additions & 0 deletions Packages/automerge-repo/Tests/AutomergeRepoTests/RepoTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import Automerge
@testable import AutomergeRepo
import AutomergeUtilities
import XCTest

final class RepoTests: XCTestCase {
let network = InMemoryNetwork.shared
var repo: Repo!

override func setUp() async throws {
repo = Repo(sharePolicy: SharePolicies.agreeable)
}

func testMostBasicRepoStartingPoints() async throws {
// Repo
// property: peers [PeerId] - all (currently) connected peers
let peers = await repo.peers()
XCTAssertEqual(peers, [])

// let peerId = await repo.peerId
// print(peerId)

// - func storageId() -> StorageId (async)
let storageId = await repo.storageId()
XCTAssertNil(storageId)

let knownIds = await repo.documentIds()
XCTAssertEqual(knownIds, [])
}

func testCreate() async throws {
let newDoc = try await repo.create()
XCTAssertNotNil(newDoc)
let knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 1)
}

func testCreateWithId() async throws {
let myId = DocumentId()
let (id, _) = try await repo.create(id: myId)
XCTAssertEqual(myId, id)

let knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 1)
XCTAssertEqual(knownIds[0], myId)
}

func testCreateWithExistingDoc() async throws {
let (id, _) = try await repo.create(doc: Document())
var knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 1)
XCTAssertEqual(knownIds[0], id)

let myId = DocumentId()
let _ = try await repo.create(doc: Document(), id: myId)
knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 2)
}

func testFind() async throws {
let myId = DocumentId()
let (id, newDoc) = try await repo.create(id: myId)
XCTAssertEqual(myId, id)

let foundDoc = try await repo.find(id: myId)
XCTAssertEqual(foundDoc.actor, newDoc.actor)
}

func testFindFailed() async throws {
do {
let _ = try await repo.find(id: DocumentId())
XCTFail()
} catch {}
}

func testDelete() async throws {
let myId = DocumentId()
let _ = try await repo.create(id: myId)
var knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 1)

try await repo.delete(id: myId)
knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 0)

do {
let _ = try await repo.find(id: DocumentId())
XCTFail()
} catch {}
}

func testClone() async throws {
let myId = DocumentId()
let (id, myCreatedDoc) = try await repo.create(id: myId)
XCTAssertEqual(myId, id)

let (newId, clonedDoc) = try await repo.clone(id: myId)
XCTAssertNotEqual(newId, id)
XCTAssertNotEqual(myCreatedDoc.actor, clonedDoc.actor)

let knownIds = await repo.documentIds()
XCTAssertEqual(knownIds.count, 2)
}

func testExportFailureUnknownId() async throws {
do {
_ = try await repo.export(id: DocumentId())
XCTFail()
} catch {}
}

func testExport() async throws {}

func testImport() async throws {}

// TBD:
// - func storageIdForPeer(peerId) -> StorageId
// - func subscribeToRemotes([StorageId])

func testRepoSetup() async throws {
let repoA = Repo(sharePolicy: SharePolicies.agreeable)
let storage = await InMemoryStorage()
await repoA.addStorageProvider(storage)

let storageId = await repoA.storageId()
XCTAssertNotNil(storageId)

// let adapter = await network.createNetworkEndpoint(config: .init(localPeerId: "onePeer", localMetaData: nil,
// listeningNetwork: false, name: "A"))
// await repoA.addNetworkAdapter(adapter: adapter)
//
// let peers = await repo.peers()
// XCTAssertEqual(peers, [])
}
}

0 comments on commit 1ebe119

Please sign in to comment.