You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`batch { }` (or `Hibiki.batch { }`) applies writes immediately but defers and deduplicates effect runs until the outermost batch exits. This is useful when you have several related writes, and you want to trigger affected effect only once (instead of triggering the effect once per write):
27
+
28
+
```ruby
29
+
first = state("Ada")
30
+
last = state("Lovelace")
31
+
effect { puts"#{first.value}#{last.value}" } # prints "Ada Lovelace"
32
+
33
+
batch do
34
+
first.value ="Grace"
35
+
last.value ="Hopper"
36
+
end# prints "Grace Hopper" — once, not twice
37
+
```
38
+
39
+
## Lifecycle: `root` and `on_cleanup`
40
+
41
+
Effects created while another effect runs are *owned* by it and disposed automatically when the owner re-runs or is disposed. For everything else there is `Hibiki.root` (Solid's `createRoot`): an ownership scope you tear down yourself — the anchor for long-lived graphs (a session, a connection) whose teardown is an external event.
42
+
43
+
`Hibiki.on_cleanup` (Solid's `onCleanup`) registers teardown on the owning effect or root; it runs before each re-run and on dispose. In other words, this is the place to release timers, sockets, subscriptions an effect sets up:
44
+
45
+
```ruby
46
+
interval = state(1)
47
+
48
+
ticker =Hibiki.root do
49
+
effect do
50
+
timer = start_timer(every: interval.value)
51
+
Hibiki.on_cleanup { timer.cancel } # runs before each re-run, and on dispose
52
+
end
53
+
end
54
+
55
+
interval.value =5# old timer cancelled, new one started
56
+
ticker.dispose # tears down every effect in the scope, cleanups included
57
+
```
58
+
59
+
A root's block runs untracked, and a root created inside an effect is *not* adopted by it — it deliberately escapes the automatic owner tree, so its lifetime is exactly `Hibiki.root` … `root.dispose`. Individual effects can still be disposed directly with `Effect#dispose`. Refer to the [Lifecycle reference]({% link _references/lifecycle-in-detail.md %}) if you wish to know more about Hibiki's lifecycle.
Svelte 5 allows `$state` / `$derived` / `$effect` as class fields; `Hibiki::Reactive` is the Ruby analogue. Declare signals with class macros and use them as plain attributes — no more `.value` in code:
effect { puts"count is now #{count}" } # starts on Counter.new
18
+
19
+
defincrement=self.count +=1
20
+
end
21
+
22
+
counter =Counter.new# prints "count is now 0"
23
+
counter.increment # prints "count is now 1"
24
+
counter.doubled # => 2
25
+
```
26
+
27
+
Signals are per-instance and created lazily; subclasses inherit all declarations. Use the block form for mutable defaults (a positional default is one shared object, the same gotcha as Rails attribute defaults).
Copy file name to clipboardExpand all lines: docs/_guides/getting-started.md
+9-111Lines changed: 9 additions & 111 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,18 +22,20 @@ gem install hibiki
22
22
23
23
## Two flavors
24
24
25
-
The DSL gives you bare `state` / `derived` / `effect` helpers. It is strictly
26
-
opt-in — the gem never includes it for you:
25
+
The DSL gives you bare `state` / `derived` / `effect` helpers. It is strictly opt-in:
27
26
28
27
```ruby
29
28
require"hibiki"
30
29
includeHibiki::DSL
31
30
32
31
x = state(0)
33
32
y = derived { x.value +1 }
33
+
34
+
x.value =10
35
+
y.value # => 11
34
36
```
35
37
36
-
Prefer no DSL? Use the classes directly — they are the same objects:
38
+
Prefer no DSL? We get you. Use the classes directly:
37
39
38
40
```ruby
39
41
require"hibiki"
@@ -47,130 +49,26 @@ y.value # => 11
47
49
48
50
## The three primitives
49
51
50
-
**`state(v)`** — a writable signal. Reading `.value` registers a dependency;
51
-
writing notifies subscribers. Writing an `==`-equal value is a no-op.
52
+
**`state(v)`** — a writable signal. Reading `.value` registers a dependency; writing notifies subscribers. Writing an `==`-equal value is a no-op.
52
53
53
54
```ruby
54
55
counter = state(0)
55
56
counter.value +=1
56
-
counter.update { it +1 } # in-place sugar
57
+
counter.update { it +1 } #Ruby's in-place sugar
57
58
```
58
59
59
-
**`derived { }`** — a lazy computed signal. It recomputes on read when marked
60
-
dirty, never on write, and caches its value until a dependency changes.
60
+
**`derived { }`** — a lazy computed signal. It recomputes on read when marked dirty (when any state it depends on changes), never on write, and caches its value until a dependency changes.
61
61
62
62
```ruby
63
63
doubled = derived { counter.value *2 }
64
64
doubled.value # => 4
65
65
```
66
66
67
-
**`effect { }`** — an eager side effect. It runs immediately and re-runs
68
-
whenever a dependency changes.
67
+
**`effect { }`** — an eager side effect. It *runs immediately* and re-runs whenever a dependency (any state inside the effect block) changes.
69
68
70
69
```ruby
71
70
name = state("world")
72
71
effect { puts"hello, #{name.value}!" } # prints "hello, world!"
73
72
74
73
name.value ="Ruby"# prints "hello, Ruby!"
75
74
```
76
-
77
-
## Untracked reads
78
-
79
-
Sometimes an effect should *sample* a signal without depending on it.
80
-
`Hibiki.untrack { }` suppresses dependency registration for a block, and
81
-
`#peek` is the per-signal shorthand — the classic use is read-modify-write,
82
-
where an effect must not depend on the signal it writes:
83
-
84
-
```ruby
85
-
count = state(0)
86
-
history = state([])
87
-
88
-
# Log every count change — without peek, writing history would re-trigger
89
-
# this effect forever (it would depend on its own output).
Copy file name to clipboardExpand all lines: docs/_guides/introduction.md
+3-13Lines changed: 3 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -5,10 +5,7 @@ nav_order: 1
5
5
6
6
# Introduction
7
7
8
-
Hibiki (響き, "resonance") brings Svelte-5-style signals to Ruby: three small
9
-
primitives — `state`, `derived`, `effect` — that track their own dependencies
10
-
at runtime. You never wire an observer or declare what depends on what; any
11
-
signal read while a computation runs subscribes it, automatically.
8
+
Hibiki (hi-bi-ki; IPA: [çi.bi.ki]) (響き, "echo, resonance") brings Svelte-5-style signals to Ruby: three small primitives — `state`, `derived`, `effect` — that track their own dependencies at runtime. You never wire an observer or declare what depends on what; any signal read while a computation runs subscribes it, automatically.
Nobody told `total` to watch `price` and `quantity`, and nobody told the
28
-
effect to watch `total` — the dependency graph assembled itself from plain
29
-
Ruby reads, and updates flow through it the moment anything changes. Writing
30
-
an equal value is a no-op, deriveds recompute lazily and only when stale, and
31
-
dependencies are re-collected on every run, so even conditional reads
32
-
(`flag ? a.value : b.value`) track correctly.
24
+
Can you see the magic? Here, I didn't tell `total` to watch `price` and `quantity`, and nobody told the effect to watch `total` — the dependency graph *assembled itself* from plain Ruby reads, and updates flow through it the moment anything changes. Writing an equal value is a no-op, deriveds recompute lazily and only when stale, and dependencies are re-collected on every run, so even conditional reads (`flag ? a.value : b.value`) track correctly.
33
25
34
-
No runtime dependencies, no magic AST rewriting — just Ruby. Head to
35
-
[Getting started]({{ "/getting-started/" | relative_url }}) to install it
36
-
and build something.
26
+
No runtime dependencies, no magic AST rewriting — just Ruby. Head to [Getting started]({{ "/getting-started/" | relative_url }}) to install it and build something.
A root's block runs untracked, and a root created inside an effect is *not* adopted by it — it deliberately escapes the automatic owner tree, so its lifetime is exactly `Hibiki.root` … `root.dispose`. Individual effects can still be disposed directly with `Effect#dispose`.
0 commit comments