Skip to content

Commit 09036ff

Browse files
committed
many changes in preparation of automerge
1 parent fd73be9 commit 09036ff

File tree

3 files changed

+145
-104
lines changed

3 files changed

+145
-104
lines changed

internal/backend/automergesyncmanager.go

Lines changed: 86 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,30 @@
11
package backend
22

33
import (
4-
"crypto/md5"
54
"errors"
65
"fmt"
76
automerge "github.com/automerge/automerge-go"
87
"github.com/gorilla/websocket"
9-
"golang.org/x/text/encoding/unicode"
108
"log"
11-
"strings"
129
mutexSync "sync"
1310
)
1411

1512
type (
16-
SyncStateRequest struct {
17-
Type string `json:"type" xml:"type" form:"type" query:"type"`
18-
RequestId string `json:"requestId" xml:"requestId" form:"requestId" query:"requestId"`
19-
DocumentId string `json:"documentId" xml:"documentId" form:"documentId" query:"documentId"`
20-
SyncState *automerge.SyncState `json:"syncState" xml:"syncState" form:"syncState" query:"syncState"`
13+
SyncRequest struct {
14+
Type string `json:"type" xml:"type" form:"type" query:"type"`
15+
RequestId string `json:"requestId" xml:"requestId" form:"requestId" query:"requestId"`
16+
DocumentId string `json:"documentId" xml:"documentId" form:"documentId" query:"documentId"`
17+
SyncState *automerge.SyncMessage `json:"syncState" xml:"syncState" form:"syncState" query:"syncState"`
2118
}
2219
)
2320

24-
// AutomergeSyncManager manages processing of EditRequests from clients
21+
// AutomergeSyncManager manages processing of SyncRequests from clients
2522
type AutomergeSyncManager struct {
2623
treeManager *TreeManager
2724
websocketConnectionManager *WebsocketConnectionManager
2825

29-
// ServerShadows client connection -> server shadow
30-
ServerShadows map[*websocket.Conn]string
26+
// automergeDocuments client connection -> automerge.Doc
27+
automergeDocuments map[*websocket.Conn]string
3128

3229
// lock for synchronizing the tree to the disk
3330
lock mutexSync.RWMutex
@@ -36,12 +33,12 @@ type AutomergeSyncManager struct {
3633
func NewAutomergeSyncManager(
3734
treeManager *TreeManager,
3835
) *AutomergeSyncManager {
39-
AutomergeSyncManager := &AutomergeSyncManager{
40-
treeManager: treeManager,
41-
ServerShadows: make(map[*websocket.Conn]string),
36+
s := &AutomergeSyncManager{
37+
treeManager: treeManager,
38+
automergeDocuments: make(map[*websocket.Conn]string),
4239
}
4340

44-
return AutomergeSyncManager
41+
return s
4542
}
4643

4744
func (sm *AutomergeSyncManager) IsItemBeingEditedRecursive(s *Section) (err error) {
@@ -63,53 +60,68 @@ func (sm *AutomergeSyncManager) IsItemBeingEditedRecursive(s *Section) (err erro
6360

6461
// sets the initial server shadow for a new client connection
6562
func (sm *AutomergeSyncManager) initClient(conn *websocket.Conn, shadowContent string) {
66-
sm.ServerShadows[conn] = shadowContent
63+
sm.automergeDocuments[conn] = shadowContent
6764
}
6865

6966
// removes the shadow for the given client
7067
func (sm *AutomergeSyncManager) removeClient(conn *websocket.Conn) {
71-
delete(sm.ServerShadows, conn)
68+
delete(sm.automergeDocuments, conn)
7269
}
7370

74-
func (sm *AutomergeSyncManager) getDocument(documentId string) (err error) {
71+
func (sm *AutomergeSyncManager) getDocument(documentId string) (doc *automerge.Doc, err error) {
7572
//doc, err := automerge.Load()
76-
doc := automerge.New()
77-
doc.
78-
//NewText(documentId)
73+
doc = automerge.New()
74+
// doc.NewText(documentId)
7975

80-
text := doc.Path("collection").Text()
76+
d := sm.treeManager.GetDocument(documentId)
77+
78+
text := doc.Path("content").Text()
79+
err = text.Set(d.Content)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
return doc, err
8185
}
8286

8387
// handles incoming edit requests from the client
84-
func (sm *AutomergeSyncManager) handleEditRequest(client *websocket.Conn, editRequest SyncStateRequest) (err error) {
85-
documentId := editRequest.DocumentId
88+
func (sm *AutomergeSyncManager) handleSyncRequest(client *websocket.Conn, syncRequest SyncRequest) (err error) {
89+
documentId := syncRequest.DocumentId
8690

8791
// get/create automerge document based on the document id
88-
automergeDocument := getDocument(documentId)
89-
// sm.ServerShadows[client]
92+
automergeDocument, err := sm.getDocument(documentId)
93+
if err != nil {
94+
log.Printf("%v: error getting document: %v", client.RemoteAddr(), err)
95+
return err
96+
}
97+
text := automergeDocument.Path("content").Text()
98+
syncState := automerge.NewSyncState(automergeDocument)
99+
// sm.automergeDocuments[client]
90100

91-
// apply patches to the automerge document
92-
automergeDocument.ApplyChanges(editRequest.Patches)
101+
_, err = syncState.ReceiveMessage(syncRequest.SyncState.Bytes())
102+
if err != nil {
103+
log.Printf("%v: error receiving sync state: %v", client.RemoteAddr(), err)
104+
return err
105+
}
93106

94-
// patch the server shadow
95-
sm.ServerShadows[client], err = ApplyPatch(sm.ServerShadows[client], editRequest.Patches)
107+
// apply patches to the automerge document
108+
for _, change := range syncRequest.SyncState.Changes() {
109+
err := automergeDocument.Apply(change)
110+
if err != nil {
111+
return err
112+
}
113+
}
96114

97115
// then patch the server document version
98116
d := sm.treeManager.GetDocument(documentId)
99-
patchedText, err := ApplyPatch(d.Content, editRequest.Patches)
100-
if err != nil {
101-
// if fuzzy patch fails, drop client changes
102-
log.Printf("%v: fuzzy patch failed: %v", client.RemoteAddr(), err)
103-
// reset err variable as we can recover from this error
104-
err = nil
105-
} else {
106-
if d.Content != patchedText {
107-
defer sm.saveCurrentDocumentContent(documentId)
108-
}
109-
d.Content = patchedText
117+
patchedText := text.String()
118+
if d.Content != patchedText {
119+
// TODO: maybe we need to save the automerge documents here too?
120+
defer sm.saveCurrentDocumentContent(documentId)
110121
}
122+
d.Content = patchedText
111123

112-
err = sm.sendEditRequestResponse(client, documentId)
124+
err = sm.sendSyncRequestResponse(client, documentId)
113125
if err != nil {
114126
log.Printf("%v: error sending response: %v", client.RemoteAddr(), err)
115127
return err
@@ -124,10 +136,9 @@ func (sm *AutomergeSyncManager) sendInitialTextResponse(client *websocket.Conn,
124136
sm.initClient(client, document.Content)
125137

126138
automergeDocument := automerge.New()
139+
syncState := automerge.NewSyncState(automergeDocument)
127140

128-
automergeText := automerge.NewText(document.Content)
129-
130-
documentContentText, err := automerge.As[*automerge.Text](automergeDocument.Root())
141+
documentContentText, err := automerge.As[*automerge.Text](automergeDocument.RootMap().Get("content"))
131142
if err != nil {
132143
return err
133144
}
@@ -138,21 +149,23 @@ func (sm *AutomergeSyncManager) sendInitialTextResponse(client *websocket.Conn,
138149

139150
commitMessage := "Initial commit"
140151
commit, err := automergeDocument.Commit(commitMessage)
152+
log.Printf("Commit: %v", commit)
141153
if err != nil {
142154
return err
143155
}
144-
serializedDocument := automergeDocument.Save()
145156

146-
syncState := automerge.NewSyncState(automergeDocument)
147-
shadowChecksum := sm.calculateChecksum(document.Content)
157+
syncStateMessage, valid := syncState.GenerateMessage()
158+
if valid == false {
159+
log.Printf("Error generating sync state message: %v", err)
160+
return err
161+
}
148162

149163
// Write current document state to the client
150-
err = client.WriteJSON(SyncStateRequest{
151-
Type: TypeInitialContent,
152-
DocumentId: document.ID,
153-
RequestId: "",
154-
SyncState: syncState,
155-
ShadowChecksum: shadowChecksum,
164+
err = client.WriteJSON(SyncRequest{
165+
Type: TypeInitialContent,
166+
DocumentId: document.ID,
167+
RequestId: "",
168+
SyncState: syncStateMessage,
156169
})
157170
if err != nil {
158171
log.Printf("%v: error writing initial content response: %v", client.RemoteAddr(), err)
@@ -163,53 +176,32 @@ func (sm *AutomergeSyncManager) sendInitialTextResponse(client *websocket.Conn,
163176
}
164177

165178
// responds to a client with the changes from the server site document version
166-
func (sm *AutomergeSyncManager) sendEditRequestResponse(client *websocket.Conn, documentId string) (err error) {
167-
d := sm.treeManager.GetDocument(documentId)
168-
169-
shadow := sm.ServerShadows[client]
170-
shadowChecksum := sm.calculateChecksum(shadow)
179+
func (sm *AutomergeSyncManager) sendSyncRequestResponse(client *websocket.Conn, documentId string) (err error) {
180+
// d := sm.treeManager.GetDocument(documentId)
171181

172-
patches, err := CreatePatch(shadow, d.Content)
182+
automergeDocument, err := sm.getDocument(documentId)
173183
if err != nil {
174-
log.Printf("Error creating patch: %v", err)
184+
log.Printf("%v: error getting document: %v", client.RemoteAddr(), err)
175185
return err
176186
}
177-
sm.ServerShadows[client] = d.Content
178187

179-
// we can skip this if there are no changes that need to be passed to the client
180-
if len(patches) <= 0 {
181-
return
188+
syncState := automerge.NewSyncState(automergeDocument)
189+
syncStateMessage, valid := syncState.GenerateMessage()
190+
if valid == false {
191+
log.Printf("Error generating sync state message: %v", err)
192+
return err
182193
}
183194

184-
return sm.websocketConnectionManager.sendToClient(client,
185-
EditRequest{
186-
Type: TypeEditRequest,
187-
RequestId: "",
188-
DocumentId: documentId,
189-
Patches: patches,
190-
ShadowChecksum: shadowChecksum,
195+
return sm.websocketConnectionManager.syncStateToClient(
196+
client,
197+
SyncRequest{
198+
Type: TypeSyncRequest,
199+
RequestId: "",
200+
DocumentId: documentId,
201+
SyncState: syncStateMessage,
191202
})
192203
}
193204

194-
// calculateChecksum calculates a checksum for a given text using the MD5 hashing algorithm.
195-
//
196-
// important notes for the implementation of this method:
197-
// - the text that is hashed must be encoded using UTF-16LE without BOM
198-
// this will ensure the bytes are the same on all clients
199-
// - the checksum string must include leading zeros
200-
// - all characters are lowercase
201-
func (sm *AutomergeSyncManager) calculateChecksum(text string) string {
202-
encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()
203-
utf16, err := encoder.String(text)
204-
if err != nil {
205-
log.Printf("Error encoding String to UTF-16: %v", err)
206-
}
207-
208-
hash := md5.Sum([]byte(utf16))
209-
checksum := fmt.Sprintf("%02x", hash[:])
210-
return strings.ToLower(checksum)
211-
}
212-
213205
func (sm *AutomergeSyncManager) saveCurrentDocumentContent(documentId string) {
214206
sm.lock.RLock()
215207
defer sm.lock.RUnlock()
@@ -237,9 +229,9 @@ func (sm *AutomergeSyncManager) SetWebsocketConnectionManager(manager *Websocket
237229
fmt.Println("New client connected", client)
238230
return sm.sendInitialTextResponse(client, document)
239231
})
240-
sm.websocketConnectionManager.SetOnIncomingMessageListener(func(client *websocket.Conn, request EditRequest) error {
241-
fmt.Println("Incoming message from client", client)
242-
return sm.handleEditRequest(client, request)
232+
sm.websocketConnectionManager.SetOnSyncRequestMessageListener(func(client *websocket.Conn, request SyncRequest) error {
233+
fmt.Println("Incoming sync message from client", client)
234+
return sm.handleSyncRequest(client, request)
243235
})
244236
sm.websocketConnectionManager.SetOnClientDisconnectedListener(func(client *websocket.Conn, documentId string, remainingConnections uint) {
245237
fmt.Println("Client disconnected", client)

internal/backend/syncmanager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (sm *SyncManager) SetWebsocketConnectionManager(manager *WebsocketConnectio
216216
fmt.Println("New client connected", client)
217217
return sm.sendInitialTextResponse(client, document)
218218
})
219-
sm.websocketConnectionManager.SetOnIncomingMessageListener(func(client *websocket.Conn, request EditRequest) error {
219+
sm.websocketConnectionManager.SetOnIncomingEditRequestMessageListener(func(client *websocket.Conn, request EditRequest) error {
220220
fmt.Println("Incoming message from client", client)
221221
return sm.handleEditRequest(client, request)
222222
})

0 commit comments

Comments
 (0)