mizchi/luna/luna/signal does not have a README file
pub trait Snapshot {
snapshot(Self) -> Self
}pub enum AsyncState[T] {
Pending
Success(T)
Failure(String)
}pub struct EffectRunner {
id : Int
run : () -> Unit
}pub struct Lens[S, A] {
get : (S) -> A
set : (S, A) -> S
}test {
let count = @signal.signal(0)
inspect(count.get(), content="0")
count.set(5)
inspect(count.get(), content="5")
}test {
let num = @signal.signal(0)
let even_only = num.filter(fn(n) { n % 2 == 0 })
inspect(even_only.get(), content="Some(0)") // 0 hasn't passed through filter yet
num.set(1) // Odd, filtered out
num.set(2) // Even, passes
inspect(even_only.get(), content="Some(2)")
num.set(3) // Odd, filtered out
inspect(even_only.get(), content="Some(2)") // Still 2
}test {
let count = @signal.signal(0)
let log : Array[Int] = []
let _ = @signal.render_effect(fn() { log.push(count.get()) })
count.set(1)
count.set(2)
inspect(log, content="[0, 1, 2]")
}test {
let count = @signal.signal(5)
let doubled = count.map(fn(n) { n * 2 })
inspect(doubled(), content="10")
count.set(7)
inspect(doubled(), content="14")
}test {
let name = @signal.Signal::new("Alice")
inspect(name.get(), content="Alice")
}test {
let count = @signal.signal(0)
let runs : Ref[Int] = { val: 0 }
let _ = @signal.render_effect(fn() {
// peek() won't create dependency
let _ = count.peek()
runs.val = runs.val + 1
})
count.set(1)
count.set(2)
// Effect only ran once (initial), not on updates
inspect(runs.val, content="1")
}test {
let count = @signal.signal(0)
count.set(42)
inspect(count.get(), content="42")
}test {
let count = @signal.signal(10)
count.update(fn(n) { n * 2 })
inspect(count.get(), content="20")
count.update(fn(n) { n + 5 })
inspect(count.get(), content="25")
}test {
let a = @signal.signal(true)
let b = @signal.signal(true)
let c = @signal.signal(false)
let all_true = @signal.all([a, b, c])
inspect(all_true(), content="false")
c.set(true)
inspect(all_true(), content="true")
a.set(false)
inspect(all_true(), content="false")
}test {
let a = @signal.signal(false)
let b = @signal.signal(false)
let any_true = @signal.any([a, b])
inspect(any_true(), content="false")
a.set(true)
inspect(any_true(), content="true")
}fn[T] batch(f : () -> T) -> Ttest {
let a = @signal.signal(0)
let b = @signal.signal(0)
let runs : Ref[Int] = { val: 0 }
let _ = @signal.render_effect(fn() {
let _ = a.get() + b.get()
runs.val = runs.val + 1
})
inspect(runs.val, content="1") // Initial run
@signal.batch(fn() {
a.set(1)
b.set(2)
})
// Only one additional run, not two
inspect(runs.val, content="2")
}test {
let first = @signal.signal("Hello")
let last = @signal.signal("World")
let full = @signal.combine2(first, last, fn(f, l) { f + " " + l })
inspect(full(), content="Hello World")
first.set("Hi")
inspect(full(), content="Hi World")
}fn[T] computed(compute : () -> T) -> (() -> T)test {
let count = @signal.signal(5)
let doubled = @signal.computed(fn() { count.get() * 2 })
inspect(doubled(), content="10")
count.set(7)
inspect(doubled(), content="14")
}fn[T] create_root(f : (() -> Unit) -> T) -> Ttest {
let count = @signal.signal(0)
let log : Array[Int] = []
let result = @signal.create_root(fn(dispose) {
let _ = @signal.render_effect(fn() { log.push(count.get()) })
dispose // Return dispose function
})
count.set(1)
result() // Dispose the root
count.set(2) // Won't trigger effect
inspect(log, content="[0, 1]")
}fn[T] create_root_with_dispose(f : () -> T) -> (T, () -> Unit)fn effect(fn_ : () -> Unit) -> (() -> Unit)// Note: effect() uses microtasks, so testing requires async handling
let count = @signal.signal(0)
let _ = @signal.effect(fn() {
println("count changed to: " + count.get().to_string())
})
count.set(1) // Effect will run after current task completesfn effect_once(fn_ : () -> Unit) -> Unittest {
let count = @signal.signal(0)
let runs : Ref[Int] = { val: 0 }
@signal.effect_once(fn() {
let _ = count.get()
runs.val = runs.val + 1
})
count.set(1)
count.set(2)
inspect(runs.val, content="2")
}fn effect_when(condition : () -> Bool, fn_ : () -> Unit) -> (() -> Unit)test {
let enabled = @signal.signal(false)
let count = @signal.signal(0)
let log : Array[Int] = []
let _ = @signal.effect_when(fn() { enabled.get() }, fn() {
log.push(count.get())
})
count.set(1) // Won't run, enabled is false
enabled.set(true)
count.set(2)
inspect(log, content="[1, 2]")
}test {
let inner1 = @signal.signal(10)
let inner2 = @signal.signal(20)
let outer = @signal.signal(inner1)
let flat = @signal.flatten(outer)
inspect(flat(), content="10")
outer.set(inner2)
inspect(flat(), content="20")
inner2.set(30)
inspect(flat(), content="30")
}fn[T] memo(compute : () -> T) -> (() -> T)test {
let a = @signal.signal(2)
let b = @signal.signal(3)
let compute_count : Ref[Int] = { val: 0 }
let sum = @signal.memo(fn() {
compute_count.val = compute_count.val + 1
a.get() + b.get()
})
inspect(sum(), content="5")
inspect(sum(), content="5") // Cached, no recompute
inspect(compute_count.val, content="1")
a.set(10)
inspect(sum(), content="13") // Recomputes
inspect(compute_count.val, content="2")
}test {
let count = @signal.signal(0)
let values : Array[Int] = []
let unsub = @signal.on(count, fn(v) { values.push(v) })
count.set(1)
count.set(2)
unsub()
count.set(3) // This won't be captured
inspect(values, content="[1, 2]")
}fn on_cleanup(cleanup : () -> Unit) -> Unittest {
let count = @signal.signal(0)
let log : Array[String] = []
let dispose = @signal.render_effect(fn() {
let v = count.get()
log.push("run:" + v.to_string())
@signal.on_cleanup(fn() { log.push("cleanup:" + v.to_string()) })
})
count.set(1)
dispose()
inspect(
log,
content=(
#|["run:0", "cleanup:0", "run:1", "cleanup:1"]
),
)
}test {
let count = @signal.signal(10)
let values : Array[Int] = []
let unsub = @signal.on_immediate(count, fn(v) { values.push(v) })
count.set(20)
unsub()
// First value (10) was captured immediately, then 20 on update
inspect(values, content="[10, 20]")
}fn on_mount(fn_ : () -> Unit) -> Unittest {
let count = @signal.signal(0)
let runs : Ref[Int] = { val: 0 }
@signal.on_mount(fn() {
let _ = count.get() // This read won't create subscription
runs.val = runs.val + 1
})
count.set(1)
count.set(2)
// on_mount only ran once, signal changes don't trigger re-run
inspect(runs.val, content="1")
}test {
let count = @signal.signal(1)
let prev = @signal.previous(count)
inspect(prev(), content="None") // No previous value yet
count.set(2)
inspect(prev(), content="Some(1)")
count.set(3)
inspect(prev(), content="Some(2)")
}test {
let count = @signal.signal(10)
let prev = @signal.previous_with_initial(count, 0)
inspect(prev(), content="0") // Initial value
count.set(20)
inspect(prev(), content="10")
count.set(30)
inspect(prev(), content="20")
}let theme_ctx = create_context("light")
provide(theme_ctx, "dark", fn() {
// All components here will see "dark"
my_component()
})fn register_disposer(disposer : () -> Unit) -> Unitfn register_owner_cleanup(cleanup : () -> Unit) -> Unitfn render_effect(fn_ : () -> Unit) -> (() -> Unit)test {
let count = @signal.signal(0)
let log : Array[String] = []
let dispose = @signal.render_effect(fn() {
log.push("count=" + count.get().to_string())
})
log.push("after-effect")
count.set(1)
dispose()
count.set(2) // Won't trigger effect
inspect(
log,
content=(
#|["count=0", "after-effect", "count=1"]
),
)
}test {
let items = @signal.signal(["a", "b", "c"])
let idx = @signal.signal(1)
let selected = @signal.select(items, idx)
inspect(
selected(),
content=(
#|Some("b")
),
)
idx.set(0)
inspect(
selected(),
content=(
#|Some("a")
),
)
idx.set(10) // Out of bounds
inspect(selected(), content="None")
}test {
let count = @signal.signal(100)
inspect(count.get(), content="100")
}test {
let flag = @signal.signal(true)
let yes = @signal.signal("YES")
let no = @signal.signal("NO")
let result = @signal.switch_(flag, yes, no)
inspect(result(), content="YES")
flag.set(false)
inspect(result(), content="NO")
no.set("NOPE")
inspect(result(), content="NOPE")
}fn[T] untracked(f : () -> T) -> Ttest {
let count = @signal.signal(0)
let runs : Ref[Int] = { val: 0 }
let _ = @signal.render_effect(fn() {
@signal.untracked(fn() {
let _ = count.get() // Won't create dependency
runs.val = runs.val + 1
})
})
count.set(1)
count.set(2)
// Effect only ran once (initial), not on updates
inspect(runs.val, content="1")
}let theme = use_context(theme_ctx) // Returns "dark" or defaulttest {
let count = @signal.signal(0)
let changes : Array[(Int, Int)] = []
let dispose = @signal.watch(fn() { count.get() }, fn(new_val, old_val) {
changes.push((new_val, old_val))
})
count.set(1)
count.set(2)
count.set(2) // Same value, no callback
count.set(3)
dispose()
inspect(changes, content="[(1, 0), (2, 1), (3, 2)]")
}test {
let count = @signal.signal(5)
let log : Array[String] = []
let dispose = @signal.watch_immediate(fn() { count.get() }, fn(
new_val,
old_val,
) {
match old_val {
Some(old) => log.push(old.to_string() + "->" + new_val.to_string())
None => log.push("init:" + new_val.to_string())
}
})
count.set(10)
dispose()
inspect(
log,
content=(
#|["init:5", "5->10"]
),
)
}