Skip to content

Commit d182d81

Browse files
committed
updating to initial automerge-repo-swift release
1 parent 2acc183 commit d182d81

File tree

5 files changed

+54
-207
lines changed

5 files changed

+54
-207
lines changed

MeetingNotes.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -799,8 +799,8 @@
799799
isa = XCRemoteSwiftPackageReference;
800800
repositoryURL = "https://github.com/automerge/automerge-repo-swift";
801801
requirement = {
802-
branch = main;
803-
kind = branch;
802+
kind = upToNextMajorVersion;
803+
minimumVersion = 0.1.0;
804804
};
805805
};
806806
/* End XCRemoteSwiftPackageReference section */

MeetingNotes/Documentation.docc/AppWalkthrough.md

Lines changed: 37 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -329,213 +329,53 @@ With a document-based SwiftUI app, the SwiftUI app framework owns the lifetime o
329329
If the file saved from the document-based app is stored in iCloud, the operating system may destroy an existing instance and re-create it from the contents on device - most notably after having replicated the file with iCloud.
330330
There may be other instances of where the document can be rebuilt, but the important aspect to note is that SwiftUI is in control of that instance's lifecycle.
331331

332-
To provide peer to peer syncing, MeetingNotes handles the document being ephemeral by enabling an app-level sync coordinator: [DocumentSyncCoordinator.swift](https://github.com/automerge/MeetingNotes/blob/main/MeetingNotes/PeerNetworking/DocumentSyncCoordinator.swift)
333-
This coordinator has properties for tracking Documents using identifiers that represent those documents and identifiers to represent the peers it syncs with.
334-
The sync coordinator presents itself as an `Observable` object for more convenient use within SwiftUI views, to provide information about peers, connections, and exposing a control to establish a new connection.
335-
336-
When MeetingNotes enables sync for a document, it registers a document with the SyncCoordinator, which builds a [NWTextRecord](https://developer.apple.com/documentation/network/nwtxtrecord) instance to use in advertising that the document.
337-
338-
```swift
339-
func registerDocument(_ document: MeetingNotesDocument) {
340-
documents[document.id] = document
341-
342-
var txtRecord = NWTXTRecord()
343-
txtRecord[TXTRecordKeys.name] = name
344-
txtRecord[TXTRecordKeys.peer_id] = peerId.uuidString
345-
txtRecord[TXTRecordKeys.doc_id] = document.id.uuidString
346-
txtRecords[document.id] = txtRecord
347-
}
348-
```
349-
350-
On activating sync, the coordinator activates both an [NWBrowser](https://developer.apple.com/documentation/network/nwbrowser) and [NWListener](https://developer.apple.com/documentation/network/nwlistener) instance.
351-
In addition to activating the network services, the coordinator starts a timer to drive checks for document updates to determine if they should send a network sync message.
352-
When a connection is established, it subscribes to the timer to drive checks to sync the Automerge document.
353-
354-
#### Network Browser
355-
356-
The browser looks for nearby peers that the app can sync with, while the listener provides the means to accept network connections.
357-
The actual sync connection can be initiated by either peer, and only one needs to be initiated to support sync.
358-
359-
The browser filters results by the type of network protocol it is initialized with: [AutomergeSyncProtocol](https://github.com/automerge/MeetingNotes/blob/main/MeetingNotes/PeerNetworking/AutomergeSyncProtocol.swift).
360-
The `NWBrowser` instance sees all available listeners, including itself, when the listener is active.
361-
The handler that processes browser updates filters the results to only show other peers on the network.
332+
To provide peer to peer syncing, MeetingNotes uses the [automerge-repo-swift package](https://github.com/automerge/automerge-repo-swift).
333+
It creates a single globally available instance of a repository to track documents that are loaded by the SwiftUI document-based app.
334+
To provide the network connections, it also creates an instance of a `WebSocketprovider` and `PeerToPeerProvider`, and adds those to the repository at the end of app initialization:
362335

363336
```swift
364-
// Only show broadcasting peers that doesn't have the name
365-
// provided by this app.
366-
let filtered = results.filter { result in
367-
if case let .bonjour(txtrecord) = result.metadata,
368-
txtrecord[TXTRecordKeys.peer_id] != self.peerId.uuidString
369-
{
370-
return true
337+
public let repo = Repo(sharePolicy: SharePolicy.agreeable)
338+
public let websocket = WebSocketProvider(.init(reconnectOnError: true))
339+
public let peerToPeer = PeerToPeerProvider(
340+
PeerToPeerProviderConfiguration(
341+
passcode: "AutomergeMeetingNotes",
342+
reconnectOnError: true,
343+
autoconnect: false
344+
)
345+
)
346+
347+
/// The document-based Meeting Notes application.
348+
@main
349+
struct MeetingNotesApp: App {
350+
...
351+
init() {
352+
Task {
353+
// Enable network adapters
354+
await repo.addNetworkAdapter(adapter: websocket)
355+
await repo.addNetworkAdapter(adapter: peerToPeer)
356+
}
371357
}
372-
return false
373358
}
374-
.sorted(by: {
375-
$0.hashValue < $1.hashValue
376-
})
377-
```
378359

379-
MeetingNotes automatically connects to a new peer listed within [NWBrowser.Result](https://developer.apple.com/documentation/network/nwbrowser/result) when running on iOS.
380-
The view that shows these results also provides a button to establish a connection manually.
381-
The auto-connect waits for a short, random period of time before establishing an automatic connection.
382-
383-
#### Network Listener
384-
385-
To accept a connection, the coordinator activates a bonjour listener for the document being shared.
386-
Within MeetingNotes, the listener is configured with the sync protocol, a `NWTxtRecord` that describes the document, and network parameters to configure TCP and TLS.
387-
MeetingNotes uses the document identifier as a pre-shared TLS secret, which both enables encryption and constraints sync connections to other instances that use this same convention.
388-
389-
> Warning: Using a pre-shared secret is _not_ a recommended security practice, and this example makes no attestations of being a secure means of encrypting the communications.
390-
391-
While the browser receives the published TXTRecord of the peer with the Bonjour notifications, the Listener only knows that it has received a connection.
392-
Because of this, at the start, who initiated the connection is unknown.
393-
MeetingNotes accepts any full connections that get fully established with TLS, using the document identifier as a shared key.
394-
A more fully developed application might also track and determine acceptability of connections using additional information - either embedded within the network sync protocol or passed as parameters within the protocol.
395-
396-
Once MeetingNotes accepts a connection, it creates an instance of [SyncConnection](https://github.com/automerge/MeetingNotes/blob/main/MeetingNotes/PeerNetworking/SyncConnection.swift).
397-
398-
#### Syncing over a connection
399-
400-
`SyncConnection` tracks the state of a connection as well as the sync state with a peer.
401-
It is initialized with a [NWConnection](https://developer.apple.com/documentation/network/nwconnection), the identifier for the document.
402-
It maintains it's own identifier and establishes an instance of `SyncState` to track the state of the peer on the other side of the connection.
403-
404-
Upon initialization, the connection wrapper subscribes to the timer provided by the sync coordinator.
405-
The `SyncConnection` uses the timer signal to drive a check to determine if a sync message should be sent.
406-
407-
```swift
408-
syncTriggerCancellable = trigger.sink(receiveValue: { _ in
409-
if let automergeDoc = sharedSyncCoordinator
410-
.documents[self.documentId]?.doc,
411-
let syncData = automergeDoc.generateSyncMessage(
412-
state: self.syncState),
413-
self.connectionState == .ready
414-
{
415-
Logger.syncConnection
416-
.info(
417-
"\(self.shortId, privacy: .public): Syncing \(syncData.count, privacy: .public) bytes to \(connection.endpoint.debugDescription, privacy: .public)"
418-
)
419-
self.sendSyncMsg(syncData)
420-
}
421-
})
422360
```
423361

424-
The underlying network protocol only sends an event if the call to `generateSyncMessage(state:)` returns non-nil data.
425-
The heart of the synchronization happens when the connection receives a network protocol sync message.
426-
This message is structured wrapper around the sync bytes from another Automerge document, along with a minimal type-of-message identifier, taking advantage of the [Network framework](https://developer.apple.com/documentation/network) to frame and establish the messages being transferred.
427-
Once received, the connection uses [NWProtocolFramer](https://developer.apple.com/documentation/network/nwprotocolframer) to retrieve the message from the bytes sent over the network, and delegates receiving the message to be processed if complete, before waiting for the next message on the network.
428-
429-
```swift
430-
private func receiveNextMessage() {
431-
guard let connection = connection else {
432-
return
433-
}
362+
The SwiftUI document-based API is all synchronous, so loading an Automerge document it provides is down within the view when it first appears.
434363

435-
connection.receiveMessage { content, context, isComplete, error in
436-
Logger.syncConnection
437-
.debug(
438-
"\(self.shortId, privacy: .public): Received a \(isComplete ? "complete" : "incomplete", privacy: .public) msg on connection"
439-
)
440-
if let content {
441-
Logger.syncConnection.debug(" - received \(content.count) bytes")
442-
} else {
443-
Logger.syncConnection.debug(" - received no data with msg")
444-
}
445-
// Extract your message type from the received context.
446-
if let syncMessage = context?
447-
.protocolMetadata(
448-
definition: AutomergeSyncProtocol.definition
449-
) as? NWProtocolFramer.Message,
450-
let endpoint = self.connection?.endpoint
451-
{
452-
self.receivedMessage(
453-
content: content,
454-
message: syncMessage,
455-
from: endpoint)
456-
}
457-
if error == nil {
458-
// Continue to receive more messages until we receive
459-
// an error.
460-
self.receiveNextMessage()
461-
} else {
462-
Logger.syncConnection.error(" - error on received message: \(error)")
463-
self.cancel()
464-
}
465-
}
466-
}
467364
```
468-
469-
The connection processes the received sync protocol message with the `receivedMessage` function, using the identifier of the document stored with the connection to retrieve a reference to the document instance.
470-
Neither the connection, nor the sync coordinator object, can maintain a stable reference to the Automerge document instance because SwiftUI owns the life-cycle of the app's `ReferenceFileDocument` subclass.
471-
To work around SwiftUI replacing this class, the coordinator maintains and updates references as `Document` subclasses register themselves, in order to provide a quick lookup by the document's identifier.
472-
473-
With a reference to the document, the method invokes `receiveSyncMessageWithPatches(state:message:)` to receive any provided changes, and uses the returns array of `Patch` to log how many patches were returned.
474-
Immediately after receiving an update, the function calls `generateSyncMessage(state:)` to determine if the additional sync messages are needed, and sends a return sync message if the function returns any data.
475-
476-
```swift
477-
func receivedMessage(
478-
content data: Data?,
479-
message: NWProtocolFramer.Message,
480-
from endpoint: NWEndpoint) {
481-
482-
guard let document = sharedSyncCoordinator.documents[self.documentId] else {
483-
// ...
484-
return
485-
}
486-
switch message.syncMessageType {
487-
case .invalid:
488-
// ...
489-
case .sync:
490-
guard let data else {
491-
// ...
492-
return
493-
}
494-
do {
495-
// When we receive a complete sync message from the
496-
// underlying transport, update our automerge document,
497-
// and the associated SyncState.
498-
let patches = try document.doc.receiveSyncMessageWithPatches(
499-
state: syncState,
500-
message: data
501-
)
502-
Logger.syncConnection
503-
.debug(
504-
"\(self.shortId, privacy: .public): Received \(patches.count, privacy: .public) patches in \(data.count, privacy: .public) bytes"
505-
)
506-
try document.getModelUpdates()
507-
508-
// Once the Automerge doc is updated, check (using the
509-
// SyncState) to see if we believe we need to send additional
510-
// messages to the peer to keep it in sync.
511-
if let response = document.doc.generateSyncMessage(state: syncState) {
512-
sendSyncMsg(response)
513-
} else {
514-
// When generateSyncMessage returns nil, the remote
515-
// endpoint represented by SyncState should be up to date.
516-
Logger.syncConnection
517-
.debug(
518-
"\(self.shortId, privacy: .public): Sync complete with \(endpoint.debugDescription, privacy: .public)"
519-
)
520-
}
521-
} catch {
522-
Logger.syncConnection
523-
.error("\(self.shortId, privacy: .public): Error applying sync message: \(error, privacy: .public)")
524-
}
525-
case .id:
526-
Logger.syncConnection.info("\(self.shortId, privacy: .public): received request for document ID")
527-
sendDocumentId(document.id.uuidString)
365+
.task {
366+
// SwiftUI controls the lifecycle of MeetingNoteDocument instances,
367+
// including sometimes regenerating them when disk contents are updated
368+
// in the background, so register the current instance with the
369+
// sync coordinator as they become visible.
370+
do {
371+
_ = try await repo.create(doc: document.doc, id: document.id)
372+
} catch {
373+
fatalError("Crashed loading the document: \(error.localizedDescription)")
528374
}
529375
}
530376
```
531377

532-
With this pattern established on both sides of a Bonjour connection, once a sync process is initiated, the functions send messages back and forth until a sync is complete.
533-
The timer, provided from the sync coordinator, is only needed to start to drive sync messages when changes have occurred locally.
534-
535-
> Note: The messages that contain changes to sync generated by Automerge are _not_ guaranteed to have all the updates needed within a single round trip.
536-
The underlying mechanism optimizes for sharing the state of heads initially, resulting in a small initial message, followed by sets of changes from either side.
537-
The full sync process is iterative, which allows for efficient sync even when the two peers may be concurrently syncing with other, unseen or unknown, peers.
538-
539-
The timer frequency in MeetingNotes is intentionally set to a short value to drive sync updates frequently enough to appear to "sync with each keystroke" to show off interactively collaboration.
540-
Your own app may not need, or want, to drive a network sync this frequently.
541-
378+
Once added to the repository, toolbar buttons on the `MeetingNotesDocumentView` toggle a WebSocket connection or activate the peer to peer networking.
379+
`PeerSyncView` provides information about available peers on your local network, and allows you to explicitly connect to those peers.
380+
The repository handles syncing automatically as the Automerge document is updated.
381+
Both the WebSocket and peer-to-peer networking implement the Automerge sync protocol over their respective transports.

MeetingNotes/Documentation.docc/Documentation.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ The source for the MeetingNotes app is [available on Github](https://github.com/
2626

2727
- ``MeetingNotesApp``
2828
- ``MergeError``
29+
- ``UserDefaultKeys``
30+
31+
### Global Variables
32+
33+
- ``repo``
34+
- ``websocket``
35+
- ``peerToPeer``
2936

3037
### Logger extensions
3138

3239
- ``MeetingNotes/os/Logger/document``
40+
- ``MeetingNotes/os/Logger/syncflow``
3341

3442
### Views
3543

@@ -53,10 +61,6 @@ The source for the MeetingNotes app is [available on Github](https://github.com/
5361
- ``ExportView_Previews``
5462
- ``WebSocketView_Previews``
5563

56-
### Legacy Sync Connection
57-
58-
- ``WebsocketSyncConnection``
59-
6064
### Application Resources
6165

6266
- ``ColorResource``

MeetingNotes/Logger+extensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ extension Logger: @unchecked Sendable {
1212
/// Logs the Document interactions, such as saving and loading.
1313
static let document = Logger(subsystem: subsystem, category: "Document")
1414

15-
/// Logs messages that might pertain to initiating, or receiving, sync updates
15+
/// Logs messages that pertain to sending or receiving sync updates.
1616
static let syncflow = Logger(subsystem: subsystem, category: "SyncFlow")
1717
}

MeetingNotes/MeetingNotesApp.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import AutomergeRepo
22
import SwiftUI
33

4-
public let repo = Repo(sharePolicy: SharePolicy.agreeable)
5-
public let websocket = WebSocketProvider(.init(reconnectOnError: true, loggingAt: .tracing))
6-
public let peerToPeer = PeerToPeerProvider(
4+
/// A global repository for storing and synchronizing Automerge documents by ID.
5+
let repo = Repo(sharePolicy: SharePolicy.agreeable)
6+
/// A WebSocket network provider for the repository.
7+
let websocket = WebSocketProvider(.init(reconnectOnError: true, loggingAt: .tracing))
8+
/// A peer-to-peer network provider for the repository.
9+
let peerToPeer = PeerToPeerProvider(
710
PeerToPeerProviderConfiguration(
811
passcode: "AutomergeMeetingNotes",
912
reconnectOnError: true,

0 commit comments

Comments
 (0)