Files
CosmicClash/server/domain/sync.go
T
2026-08-31 20:51:46 +01:00

82 lines
2.0 KiB
Go

package domain
import "fmt"
var (
ErrRevisionGap = fmt.Errorf("revision gap requires resync")
ErrSyncConflict = fmt.Errorf("conflicting revisioned event")
)
type SyncEvent struct {
Kind ResourceKind
ResourceID string
Revision uint64
State State
}
type ReplicaResource struct {
Kind ResourceKind
ResourceID string
State State
Revision uint64
NeedsResync bool
}
func NewReplicaResource(kind ResourceKind, resourceID string, state State) (*ReplicaResource, error) {
if resourceID == "" || !validStateForKind(kind, state) {
return nil, fmt.Errorf("invalid replica resource")
}
return &ReplicaResource{Kind: kind, ResourceID: resourceID, State: state}, nil
}
// ApplyEvent makes duplicate/out-of-order delivery converge. A gap is not
// guessed through; callers must fetch the authoritative REST snapshot and use
// ReplaceSnapshot before resuming the event stream.
func (r *ReplicaResource) ApplyEvent(event SyncEvent) error {
if event.Kind != r.Kind || event.ResourceID != r.ResourceID {
return ErrSyncConflict
}
if r.NeedsResync {
return ErrRevisionGap
}
if event.Revision <= r.Revision {
if event.Revision == r.Revision && event.State != r.State {
return ErrSyncConflict
}
return nil
}
if event.Revision != r.Revision+1 {
r.NeedsResync = true
return ErrRevisionGap
}
if !legalTransition(r.Kind, r.State, event.State) {
return ErrSyncConflict
}
r.State, r.Revision = event.State, event.Revision
return nil
}
func (r *ReplicaResource) ReplaceSnapshot(revision uint64, state State) error {
if revision < r.Revision || !validStateForKind(r.Kind, state) {
return ErrSyncConflict
}
r.State, r.Revision, r.NeedsResync = state, revision, false
return nil
}
func validStateForKind(kind ResourceKind, state State) bool {
switch kind {
case ResourceQueueTicket:
_, ok := queueTransitions[state]
return ok || state == Queued
case ResourceProposal:
_, ok := proposalTransitions[state]
return ok || state == Open
case ResourceMatch:
_, ok := matchTransitions[state]
return ok
default:
return false
}
}