guesstimate/server/internal/diffsync/peer.go

152 lines
3.2 KiB
Go

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
}