package diffsync import ( "log" "sync" "github.com/pkg/errors" ) type Peer struct { document *Document shadow *Shadow mutex sync.RWMutex editsStack EditsStack } func (p *Peer) Document() *Document { return p.document } func (p *Peer) Shadow() *Shadow { return p.shadow } func (p *Peer) Update(content []byte) (EditsStack, error) { p.mutex.Lock() defer p.mutex.Unlock() ops, err := p.document.differ.Diff(content, p.shadow.content) if err != nil { return nil, err } p.editsStack = append(p.editsStack, &Edits{ ops: ops, localVersion: p.shadow.localVersion, remoteVersion: p.shadow.remoteVersion, }) p.shadow.localVersion++ p.shadow.content = content p.document.content = content newStack := make(EditsStack, len(p.editsStack)) copy(newStack, p.editsStack) return newStack, nil } func (p *Peer) Apply(stack EditsStack) error { p.mutex.Lock() defer p.mutex.Unlock() newStack := make(EditsStack, len(p.editsStack)) copy(newStack, p.editsStack) for _, edits := range stack { rollbacked, err := p.applyEdits(edits) if err != nil { return errors.Wrap(err, "could not apply edits") } if rollbacked { newStack = EditsStack{} } // Iterate over local edits for i, localEdits := range newStack { if localEdits.LocalVersion() != edits.RemoteVersion() { continue } log.Printf("removing edits %d", localEdits.LocalVersion()) // Remove local edit copy(newStack[i:], newStack[i+1:]) newStack[len(newStack)-1] = nil newStack = newStack[:len(newStack)-1] } } p.editsStack = newStack return nil } func (p *Peer) applyEdits(edits *Edits) (bool, error) { rollbacked := false shadow := p.Shadow() var ( err error newDocumentContent []byte newShadowRemoteVersion Version ) newShadowContent := shadow.content newShadowLocalVersion := shadow.localVersion log.Printf( "shadow(l: %d, r: %d) edits(l: %d, r: %d)", shadow.LocalVersion(), shadow.RemoteVersion(), edits.LocalVersion(), edits.RemoteVersion(), ) // Desync occurred, rollback to backup if possible if shadow.LocalVersion() > edits.RemoteVersion() { if shadow.Backup().LocalVersion() != edits.RemoteVersion() { return false, ErrUnexpectedRemoteVersion } newShadowContent = shadow.backup.Content() newShadowLocalVersion = shadow.backup.LocalVersion() rollbacked = true } if edits.LocalVersion() < shadow.RemoteVersion() { return rollbacked, nil } if edits.RemoteVersion() < newShadowLocalVersion { return rollbacked, ErrInvalidState } newShadowContent, err = p.document.patcher.Patch(newShadowContent, edits.Ops()) if err != nil { return false, errors.Wrap(err, "could not patch shadow content") } newDocumentContent, err = p.document.patcher.Patch(p.document.content, edits.Ops()) if err != nil { return rollbacked, errors.Wrap(err, "could not patch document content") } newShadowRemoteVersion++ // Update shadow shadow.content = newShadowContent shadow.remoteVersion = newShadowRemoteVersion shadow.localVersion = newShadowLocalVersion // Update document p.document.content = newDocumentContent // Update backup shadow.backup.content = newShadowContent shadow.backup.localVersion = shadow.localVersion return rollbacked, nil }