Package: github.com/TIVerse/drav/pkg/prana
Prana (Sanskrit: प्राण, "life force, vital energy") provides reactive state management for DRAV applications. Observable values automatically trigger UI re-renders when changed.
Observables are generic containers that notify watchers on change:
counter := prana.NewObservable(0)
counter.Set(42) // UI re-renders automatically
value := counter.Get() // Returns 42When an observable changes, DRAV automatically triggers a re-render:
type Counter struct {
count *prana.Observable[int]
}
func (c *Counter) increment() {
c.count.Update(func(n int) int { return n + 1 })
// UI updates automatically - no manual refresh needed!
}Observables are fully generic and type-safe:
stringObs := prana.NewObservable[string]("hello")
intObs := prana.NewObservable[int](42)
structObs := prana.NewObservable[User](User{Name: "Alice"})obs := prana.NewObservable(initialValue)value := obs.Get() // Thread-safe readobs.Set(newValue) // Triggers watchers and re-renderApply a function to transform the value:
obs.Update(func(current int) int {
return current + 1
})Register callbacks for change notifications:
unwatch := obs.Watch(func(oldVal, newVal int) {
fmt.Printf("Changed from %d to %d\n", oldVal, newVal)
})
// Clean up when done
defer unwatch()Derive observables from others:
celsius := prana.NewObservable(0.0)
fahrenheit := celsius.Derive(func(c float64) float64 {
return c*9/5 + 32
})
celsius.Set(100.0)
fmt.Println(fahrenheit.Get()) // 212.0Update multiple observables without triggering multiple renders:
batch := prana.NewBatch()
batch.Add(func() {
name.Set("Alice")
age.Set(30)
city.Set("NYC")
})
batch.Execute() // Single re-render for all changesExecute side effects when observables change:
effect := prana.NewEffect(func() {
// This runs when dependencies change
fmt.Printf("Count is now: %d\n", counter.Get())
}, counter)
defer effect.Dispose()Organize related state with actions and reducers:
type AppState struct {
Count int
Username string
}
store := prana.NewStore(AppState{Count: 0})
// Register reducer
store.AddReducer("increment", func(state AppState, payload any) AppState {
state.Count++
return state
})
// Dispatch action
store.Dispatch("increment", nil)type TodoList struct {
items *prana.Observable[[]Todo]
filter *prana.Observable[FilterType]
}
func NewTodoList() *TodoList {
return &TodoList{
items: prana.NewObservable([]Todo{}),
filter: prana.NewObservable(FilterAll),
}
}
func (t *TodoList) addTodo(text string) {
t.items.Update(func(items []Todo) []Todo {
return append(items, Todo{Text: text, Done: false})
})
}type Dashboard struct {
data *prana.Observable[[]DataPoint]
filtered *prana.Observable[[]DataPoint]
sortOrder *prana.Observable[SortOrder]
}
func NewDashboard() *Dashboard {
d := &Dashboard{
data: prana.NewObservable([]DataPoint{}),
sortOrder: prana.NewObservable(SortAsc),
}
// filtered updates when data or sortOrder changes
d.filtered = prana.NewComputed(func() []DataPoint {
data := d.data.Get()
order := d.sortOrder.Get()
return sortData(data, order)
}, d.data, d.sortOrder)
return d
}type LoginForm struct {
username *prana.Observable[string]
password *prana.Observable[string]
valid *prana.Observable[bool]
}
func NewLoginForm() *LoginForm {
f := &LoginForm{
username: prana.NewObservable(""),
password: prana.NewObservable(""),
}
// Auto-validate
f.valid = prana.NewComputed(func() bool {
return len(f.username.Get()) > 0 && len(f.password.Get()) >= 8
}, f.username, f.password)
return f
}Create observables during component construction:
func NewCounter() *Counter {
return &Counter{
count: prana.NewObservable(0), // Good
}
}Always clean up watchers to prevent memory leaks:
func (c *Component) init() {
unwatch := observable.Watch(handler)
c.cleanup = append(c.cleanup, unwatch)
}
func (c *Component) dispose() {
for _, fn := range c.cleanup {
fn()
}
}Don't duplicate state - compute it:
// Bad
type BadComponent struct {
celsius *prana.Observable[float64]
fahrenheit *prana.Observable[float64] // Duplicate state!
}
// Good
type GoodComponent struct {
celsius *prana.Observable[float64]
fahrenheit *prana.Observable[float64] // Computed from celsius
}// Bad - multiple re-renders
user.name.Set("Alice")
user.age.Set(30)
user.city.Set("NYC")
// Good - single re-render
batch := prana.NewBatch()
batch.Add(func() {
user.name.Set("Alice")
user.age.Set(30)
user.city.Set("NYC")
})
batch.Execute()Expose methods instead of raw observables:
type Counter struct {
count *prana.Observable[int] // Private
}
func (c *Counter) Get() int {
return c.count.Get()
}
func (c *Counter) Increment() {
c.count.Update(func(n int) int { return n + 1 })
}type Counter struct {
count *prana.Observable[int]
}
func NewCounter() *Counter {
return &Counter{
count: prana.NewObservable(0),
}
}
func (c *Counter) Increment() {
c.count.Update(func(n int) int { return n + 1 })
}
func (c *Counter) Render(ctx maya.RenderContext) maya.View {
return maya.Column(
maya.Text(fmt.Sprintf("Count: %d", c.count.Get())),
maya.Button("+", c.Increment),
)
}type Todo struct {
Text string
Done bool
}
type TodoList struct {
items *prana.Observable[[]Todo]
}
func (t *TodoList) Add(text string) {
t.items.Update(func(items []Todo) []Todo {
return append(items, Todo{Text: text, Done: false})
})
}
func (t *TodoList) Toggle(index int) {
t.items.Update(func(items []Todo) []Todo {
items[index].Done = !items[index].Done
return items
})
}All operations are thread-safe but come with a cost:
// Fine for UI updates (infrequent)
obs.Set(value)
// For high-frequency updates, consider batching
batch.Add(func() {
obs1.Set(v1)
obs2.Set(v2)
})Each watcher adds overhead. Limit watchers:
// Bad - too many watchers
for i := 0; i < 1000; i++ {
obs.Watch(handlers[i])
}
// Good - single watcher with dispatch
obs.Watch(func(old, new any) {
for _, handler := range handlers {
handler(old, new)
}
})Always unsubscribe:
unwatch := obs.Watch(handler)
defer unwatch() // Don't forget!Ensure the observable is being modified, not replaced:
// Bad
myComponent.items = newItems // Direct assignment - no update!
// Good
myComponent.items.Set(newItems) // Triggers updateAvoid watching an observable and modifying it in the watcher:
// Bad - infinite loop!
obs.Watch(func(old, new int) {
obs.Set(new + 1) // Triggers another change!
})
// Use computed instead
derived := obs.Derive(func(n int) int { return n + 1 })