11package backend
22
33import (
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
1512type (
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
2522type 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 {
3633func 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
4744func (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
6562func (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
7067func (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-
213205func (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 )
0 commit comments