152 lines
3.2 KiB
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
|
|
}
|