Skip to content

Commit e2f21a9

Browse files
committed
Improve doc site
1 parent cceaea7 commit e2f21a9

12 files changed

Lines changed: 151 additions & 131 deletions

docs/_config.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,19 @@ plugins:
6161
jekyll_vitepress:
6262
branding:
6363
site_title: Hibiki Documentation
64+
logo:
65+
default: /assets/img/logo.svg
66+
alt: Hibiki Logo
67+
width: 24
68+
height: 24
6469
syntax:
6570
light_theme: github
6671
dark_theme: github.dark
72+
github_star:
73+
enabled: true
74+
repository: planetaska/hibiki
75+
text: Star
76+
show_count: true
6777

6878
# Collections for Jekyll VitePress
6979
collections:

docs/_data/social_links.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
- icon: x
2+
url: https://x.com/planetaska
3+
label: X
4+
- icon: bluesky
5+
url: https://bsky.app/profile/aska-konomi.bsky.social
6+
label: Bluesky

docs/_guides/advanced-usage.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
title: Advanced Usage
3+
nav_order: 4
4+
---
5+
6+
# Advanced Usage
7+
8+
## Untracked reads
9+
10+
Sometimes an effect should *sample* a signal without depending on it.
11+
`Hibiki.untrack { }` suppresses dependency registration for a block, and
12+
`#peek` is the per-signal shorthand — the classic use is read-modify-write,
13+
where an effect must not depend on the signal it writes:
14+
15+
```ruby
16+
count = state(0)
17+
history = state([])
18+
19+
# Log every count change — without peek, writing history would re-trigger
20+
# this effect forever (it would depend on its own output).
21+
effect { history.value = history.peek + [count.value] }
22+
```
23+
24+
## Batching
25+
26+
`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.
60+
61+
## Where to next
62+
63+
- [Threading model]({{ "/threading-model/" | relative_url }}) —
64+
fiber-confined bookkeeping, what is and isn't isolated across threads,
65+
fibers, and Ractors.
66+
- [Why no transparent signals?]({{ "/why-no-transparent-signals/" | relative_url }}) —
67+
the rejected transparency designs, with the failure cases spelled out.
68+
- [Status & limitations]({{ "/status-and-limitations/" | relative_url }}) —
69+
what the signal core already guarantees.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
title: Class-based reactivity
3+
nav_order: 3
4+
---
5+
6+
# Class-based reactivity
7+
8+
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:
9+
10+
```ruby
11+
class Counter
12+
include Hibiki::Reactive
13+
14+
state :count, 0
15+
state(:history) { [] } # block form: fresh default per instance
16+
derived(:doubled) { count * 2 }
17+
effect { puts "count is now #{count}" } # starts on Counter.new
18+
19+
def increment = 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).

docs/_guides/getting-started.md

Lines changed: 9 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ gem install hibiki
2222

2323
## Two flavors
2424

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:
2726

2827
```ruby
2928
require "hibiki"
3029
include Hibiki::DSL
3130

3231
x = state(0)
3332
y = derived { x.value + 1 }
33+
34+
x.value = 10
35+
y.value # => 11
3436
```
3537

36-
Prefer no DSL? Use the classes directly — they are the same objects:
38+
Prefer no DSL? We get you. Use the classes directly:
3739

3840
```ruby
3941
require "hibiki"
@@ -47,130 +49,26 @@ y.value # => 11
4749

4850
## The three primitives
4951

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.
5253

5354
```ruby
5455
counter = state(0)
5556
counter.value += 1
56-
counter.update { it + 1 } # in-place sugar
57+
counter.update { it + 1 } # Ruby's in-place sugar
5758
```
5859

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.
6161

6262
```ruby
6363
doubled = derived { counter.value * 2 }
6464
doubled.value # => 4
6565
```
6666

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.
6968

7069
```ruby
7170
name = state("world")
7271
effect { puts "hello, #{name.value}!" } # prints "hello, world!"
7372

7473
name.value = "Ruby" # prints "hello, Ruby!"
7574
```
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).
90-
effect { history.value = history.peek + [count.value] }
91-
```
92-
93-
## Batching
94-
95-
`batch { }` (or `Hibiki.batch { }`) applies writes immediately but defers and
96-
deduplicates effect runs until the outermost batch exits — several related
97-
writes trigger each affected effect once, not once per write:
98-
99-
```ruby
100-
first = state("Ada")
101-
last = state("Lovelace")
102-
effect { puts "#{first.value} #{last.value}" } # prints "Ada Lovelace"
103-
104-
batch do
105-
first.value = "Grace"
106-
last.value = "Hopper"
107-
end # prints "Grace Hopper" — once, not twice
108-
```
109-
110-
## Lifecycle: `root` and `on_cleanup`
111-
112-
Effects created while another effect runs are *owned* by it and disposed
113-
automatically when the owner re-runs or is disposed. For everything else
114-
there is `Hibiki.root` (Solid's `createRoot`): an ownership scope you tear
115-
down yourself — the anchor for long-lived graphs (a session, a connection)
116-
whose teardown is an external event, not a rerun.
117-
118-
`Hibiki.on_cleanup` (Solid's `onCleanup`) registers teardown on the owning
119-
effect or root; it runs before each re-run and on dispose — the place to
120-
release timers, sockets, subscriptions an effect sets up:
121-
122-
```ruby
123-
interval = state(1)
124-
125-
ticker = Hibiki.root do
126-
effect do
127-
timer = start_timer(every: interval.value)
128-
Hibiki.on_cleanup { timer.cancel } # runs before each re-run, and on dispose
129-
end
130-
end
131-
132-
interval.value = 5 # old timer cancelled, new one started
133-
ticker.dispose # tears down every effect in the scope, cleanups included
134-
```
135-
136-
A root's block runs untracked, and a root created inside an effect is *not*
137-
adopted by it — it deliberately escapes the automatic owner tree, so its
138-
lifetime is exactly `Hibiki.root``root.dispose`. Individual effects can
139-
still be disposed directly with `Effect#dispose`.
140-
141-
## Class-based reactivity
142-
143-
Svelte 5 allows `$state` / `$derived` / `$effect` as class fields;
144-
`Hibiki::Reactive` is the Ruby analogue. Declare signals with class macros and
145-
use them as plain attributes — no `.value` at usage sites:
146-
147-
```ruby
148-
class Counter
149-
include Hibiki::Reactive
150-
151-
state :count, 0
152-
state(:history) { [] } # block form: fresh default per instance
153-
derived(:doubled) { count * 2 }
154-
effect { puts "count is now #{count}" } # starts on Counter.new
155-
156-
def increment = self.count += 1
157-
end
158-
159-
counter = Counter.new # prints "count is now 0"
160-
counter.increment # prints "count is now 1"
161-
counter.doubled # => 2
162-
```
163-
164-
Signals are per-instance and created lazily; subclasses inherit all
165-
declarations. Use the block form for mutable defaults (a positional default
166-
is one shared object, the same gotcha as Rails attribute defaults).
167-
168-
## Where to next
169-
170-
- [Threading model]({{ "/threading-model/" | relative_url }}) —
171-
fiber-confined bookkeeping, what is and isn't isolated across threads,
172-
fibers, and Ractors.
173-
- [Why no transparent signals?]({{ "/why-no-transparent-signals/" | relative_url }}) —
174-
the rejected transparency designs, with the failure cases spelled out.
175-
- [Status & limitations]({{ "/status-and-limitations/" | relative_url }}) —
176-
what the signal core already guarantees.

docs/_guides/introduction.md

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ nav_order: 1
55

66
# Introduction
77

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.
129

1310
```ruby
1411
require "hibiki"
@@ -24,13 +21,6 @@ quantity.value = 3 # prints "Total: $300"
2421
price.value = 50 # prints "Total: $150"
2522
```
2623

27-
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.
3325

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.

docs/_references/fragment-level-render-effects.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Fragment-level render effects
3-
nav_order: 4
3+
nav_order: 5
44
---
55

66
# Fragment-level render effects (a future direction)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Lifecycle in detail
3+
nav_order: 2
4+
---
5+
6+
# Lifecycle in detail
7+
8+
Expand on the following:
9+
10+
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`.

docs/_references/status-and-limitations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Status & limitations
3-
nav_order: 3
3+
nav_order: 4
44
---
55

66
# Status & limitations

docs/_references/why-no-transparent-signals.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Why no transparent signals?
3-
nav_order: 2
3+
nav_order: 3
44
---
55

66
# Why no transparent signals?

0 commit comments

Comments
 (0)