waveterm/tsunami/app/atom.go
2025-11-07 18:19:52 -08:00

139 lines
4.2 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package app
import (
"log"
"reflect"
"runtime"
"github.com/wavetermdev/waveterm/tsunami/engine"
"github.com/wavetermdev/waveterm/tsunami/util"
)
// AtomMeta provides metadata about an atom for validation and documentation
type AtomMeta struct {
Desc string // short, user-facing
Units string // "ms", "GiB", etc.
Min *float64 // optional minimum (numeric types)
Max *float64 // optional maximum (numeric types)
Enum []string // allowed values if finite set
Pattern string // regex constraint for strings
}
// SecretMeta provides metadata about a secret for documentation and validation
type SecretMeta struct {
Desc string
Optional bool
}
// Atom[T] represents a typed atom implementation
type Atom[T any] struct {
name string
client *engine.ClientImpl
}
// logInvalidAtomSet logs an error when an atom is being set during component render
func logInvalidAtomSet(atomName string) {
_, file, line, ok := runtime.Caller(2)
if ok {
log.Printf("invalid Set of atom '%s' in component render function at %s:%d", atomName, file, line)
} else {
log.Printf("invalid Set of atom '%s' in component render function", atomName)
}
}
// sameRef returns true if oldVal and newVal share the same underlying reference
// (pointer, map, or slice). Nil values return false.
func sameRef[T any](oldVal, newVal T) bool {
vOld := reflect.ValueOf(oldVal)
vNew := reflect.ValueOf(newVal)
if !vOld.IsValid() || !vNew.IsValid() {
return false
}
switch vNew.Kind() {
case reflect.Ptr:
// direct comparison works for *T
return any(oldVal) == any(newVal)
case reflect.Map, reflect.Slice:
if vOld.Kind() != vNew.Kind() || vOld.IsZero() || vNew.IsZero() {
return false
}
return vOld.Pointer() == vNew.Pointer()
}
// primitives, structs, etc. → not a reference type
return false
}
// logMutationWarning logs a warning when mutation is detected
func logMutationWarning(atomName string) {
_, file, line, ok := runtime.Caller(2)
if ok {
log.Printf("WARNING: atom '%s' appears to be mutated instead of copied at %s:%d - use app.DeepCopy to create a copy before mutating", atomName, file, line)
} else {
log.Printf("WARNING: atom '%s' appears to be mutated instead of copied - use app.DeepCopy to create a copy before mutating", atomName)
}
}
// AtomName implements the vdom.Atom interface
func (a Atom[T]) AtomName() string {
return a.name
}
// Get returns the current value of the atom. When called during component render,
// it automatically registers the component as a dependency for this atom, ensuring
// the component re-renders when the atom value changes.
func (a Atom[T]) Get() T {
vc := engine.GetGlobalRenderContext()
if vc != nil {
vc.UsedAtoms[a.name] = true
}
val := a.client.Root.GetAtomVal(a.name)
typedVal := util.GetTypedAtomValue[T](val, a.name)
return typedVal
}
// Set updates the atom's value to the provided new value and triggers re-rendering
// of any components that depend on this atom. This method cannot be called during
// render cycles - use effects or event handlers instead.
func (a Atom[T]) Set(newVal T) {
vc := engine.GetGlobalRenderContext()
if vc != nil {
logInvalidAtomSet(a.name)
return
}
// Check for potential mutation bugs with reference types
currentVal := a.client.Root.GetAtomVal(a.name)
currentTyped := util.GetTypedAtomValue[T](currentVal, a.name)
if sameRef(currentTyped, newVal) {
logMutationWarning(a.name)
}
if err := a.client.Root.SetAtomVal(a.name, newVal); err != nil {
log.Printf("Failed to set atom value for %s: %v", a.name, err)
return
}
a.client.Root.AtomAddRenderWork(a.name)
}
// SetFn updates the atom's value by applying the provided function to the current value.
// The function receives a copy of the current atom value, which can be safely mutated
// without affecting the original data. The return value from the function becomes the
// new atom value. This method cannot be called during render cycles.
func (a Atom[T]) SetFn(fn func(T) T) {
vc := engine.GetGlobalRenderContext()
if vc != nil {
logInvalidAtomSet(a.name)
return
}
currentVal := a.Get()
copiedVal := DeepCopy(currentVal)
newVal := fn(copiedVal)
a.Set(newVal)
}