go-lns/pkg/primitives/view.go
Virgil bc1d37e2dc feat(primitives): preserve name undo order
Co-authored-by: Virgil <virgil@lethean.io>
2026-04-02 03:20:17 +00:00

222 lines
4.8 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package primitives
import (
"bytes"
"encoding/binary"
"fmt"
"sort"
)
// NameStateGetter is the minimal lookup interface used by NameView.
type NameStateGetter interface {
GetNameState(Hash) (*NameState, error)
}
// NameView caches name states during state-tree processing.
type NameView struct {
names map[Hash]*NameState
order []Hash
}
// NewNameView creates an empty name-state view.
func NewNameView() *NameView {
return &NameView{
names: make(map[Hash]*NameState),
}
}
func (v *NameView) ensureCache() {
if v.names == nil {
v.names = make(map[Hash]*NameState)
}
}
// GetNameStateSync returns the cached name state or loads it from db.
//
// When the database has no entry for the requested hash, a new empty state is
// created with NameHash populated, mirroring the JS reference behavior.
func (v *NameView) GetNameStateSync(db NameStateGetter, nameHash Hash) (*NameState, error) {
v.ensureCache()
if ns, ok := v.names[nameHash]; ok {
return ns, nil
}
if db != nil {
ns, err := db.GetNameState(nameHash)
if err != nil {
return nil, err
}
if ns != nil {
v.names[nameHash] = ns
v.order = append(v.order, nameHash)
return ns, nil
}
}
ns := &NameState{NameHash: nameHash}
v.names[nameHash] = ns
v.order = append(v.order, nameHash)
return ns, nil
}
// GetNameState is an alias for GetNameStateSync.
func (v *NameView) GetNameState(db NameStateGetter, nameHash Hash) (*NameState, error) {
return v.GetNameStateSync(db, nameHash)
}
// ToNameUndo converts the cached view into a sparse undo record.
func (v *NameView) ToNameUndo() NameUndo {
undo := NameUndo{}
if v == nil {
return undo
}
return undo.FromView(v)
}
// NameUndoEntry pairs a name hash with its sparse state delta.
type NameUndoEntry struct {
NameHash Hash
Delta NameDelta
}
// NameUndo stores the deltas required to roll a name view back.
type NameUndo struct {
Names []NameUndoEntry
}
// FromView populates the undo record from a cached view.
func (u *NameUndo) FromView(view *NameView) NameUndo {
if view == nil {
*u = NameUndo{}
return *u
}
u.Names = u.Names[:0]
seen := make(map[Hash]struct{}, len(view.order))
appendEntry := func(hash Hash, ns *NameState) {
if ns == nil || !ns.HasDelta() {
return
}
u.Names = append(u.Names, NameUndoEntry{
NameHash: hash,
Delta: *ns.Delta(),
})
}
for _, hash := range view.order {
ns, ok := view.names[hash]
if !ok {
continue
}
seen[hash] = struct{}{}
appendEntry(hash, ns)
}
if len(seen) != len(view.names) {
remaining := make([]Hash, 0, len(view.names)-len(seen))
for hash := range view.names {
if _, ok := seen[hash]; ok {
continue
}
remaining = append(remaining, hash)
}
sort.Slice(remaining, func(i, j int) bool {
return bytes.Compare(remaining[i][:], remaining[j][:]) < 0
})
for _, hash := range remaining {
appendEntry(hash, view.names[hash])
}
}
return *u
}
// GetSize returns the serialized size of the undo payload.
func (u NameUndo) GetSize() int {
size := 4
for _, entry := range u.Names {
size += len(entry.NameHash)
size += entry.Delta.GetSize()
}
return size
}
// MarshalBinary serializes the undo payload.
func (u NameUndo) MarshalBinary() ([]byte, error) {
buf := make([]byte, 0, u.GetSize())
var tmp [4]byte
binary.LittleEndian.PutUint32(tmp[:], uint32(len(u.Names)))
buf = append(buf, tmp[:]...)
for _, entry := range u.Names {
buf = append(buf, entry.NameHash[:]...)
delta, err := entry.Delta.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("primitives.NameUndo.MarshalBinary: %w", err)
}
buf = append(buf, delta...)
}
return buf, nil
}
// UnmarshalBinary decodes the undo payload.
func (u *NameUndo) UnmarshalBinary(data []byte) error {
*u = NameUndo{}
if len(data) < 4 {
return fmt.Errorf("primitives.NameUndo.UnmarshalBinary: short buffer")
}
count := binary.LittleEndian.Uint32(data[:4])
data = data[4:]
u.Names = make([]NameUndoEntry, 0, int(count))
for i := uint32(0); i < count; i++ {
if len(data) < len(Hash{}) {
return fmt.Errorf("primitives.NameUndo.UnmarshalBinary: short buffer")
}
var hash Hash
copy(hash[:], data[:len(hash)])
data = data[len(hash):]
delta, consumed, err := decodeNameDeltaPrefix(data)
if err != nil {
return fmt.Errorf("primitives.NameUndo.UnmarshalBinary: %w", err)
}
u.Names = append(u.Names, NameUndoEntry{
NameHash: hash,
Delta: delta,
})
data = data[consumed:]
}
if len(data) != 0 {
return fmt.Errorf("primitives.NameUndo.UnmarshalBinary: trailing data")
}
return nil
}
func decodeNameDeltaPrefix(data []byte) (NameDelta, int, error) {
for n := 4; n <= len(data); n++ {
var delta NameDelta
if err := delta.UnmarshalBinary(data[:n]); err == nil {
return delta, n, nil
}
}
return NameDelta{}, 0, fmt.Errorf("short buffer")
}