hibiki 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +6 -4
- data/lib/hibiki/derived.rb +5 -1
- data/lib/hibiki/dsl.rb +2 -2
- data/lib/hibiki/reactive.rb +4 -4
- data/lib/hibiki/state.rb +12 -2
- data/lib/hibiki/tracking.rb +23 -7
- data/lib/hibiki/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62668f45023a6d59c07b6f414b0c6ec50a7ff264b5dc9d2b6ad0bedfec9510cc
|
|
4
|
+
data.tar.gz: 9391c0a9b3865c61a0c6cc7f2463e3a377cac91b89820fd3cb3c34dffcc52ddc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e425aa81829367bd15d732dd2dd3a2f7fe854199a03df742c4d3e764f0a01e756fc2af81c8d14582630884fc4314d98b3750851554c9da34c0b3b9825da1bf45
|
|
7
|
+
data.tar.gz: dd3e43c2bb9e9f6b399feb2da484913837646ec81bb9d5d55c2f41c31883270e2996d062a59c01497376f10005c49c76c5fa54a2ba0eac683afdea15d33f9a75
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-08-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`equals:` — a per-signal equality override** on `State` and `Derived`, and
|
|
15
|
+
passed through by the `state`/`derived` helpers in `Hibiki::DSL` and
|
|
16
|
+
`Hibiki::Reactive`. Solid's `createSignal(value, { equals })` shape:
|
|
17
|
+
omitted/`nil` keeps `==`; a callable is a custom comparator, called with
|
|
18
|
+
`(prev, next)`, truthy meaning "unchanged"; `equals: false` means every write
|
|
19
|
+
notifies. The override is honored at both places equality guards the graph —
|
|
20
|
+
the write gate (`State#value=`) and the effect equality gate at the batch
|
|
21
|
+
flush (new `Trackable#changed_from?`, consulted by
|
|
22
|
+
`Observer#sources_changed?`). Signals that don't pass `equals:` behave
|
|
23
|
+
exactly as before.
|
|
24
|
+
|
|
10
25
|
## [0.2.0] - 2026-07-29
|
|
11
26
|
|
|
12
27
|
### Changed
|
data/README.md
CHANGED
|
@@ -46,7 +46,7 @@ y.value # => 11
|
|
|
46
46
|
|
|
47
47
|
### The three primitives
|
|
48
48
|
|
|
49
|
-
**`state(v)`** — a writable signal. Reading `.value` registers a dependency; writing notifies subscribers. Writing an `==`-equal value is a no-op.
|
|
49
|
+
**`state(v)`** — a writable signal. Reading `.value` registers a dependency; writing notifies subscribers. Writing an `==`-equal value is a no-op — overridable per signal with `equals:`; see [Custom equality](https://planetaska.github.io/hibiki/advanced-usage/#custom-equality-equals).
|
|
50
50
|
|
|
51
51
|
```ruby
|
|
52
52
|
counter = state(0)
|
|
@@ -108,13 +108,15 @@ counter.increment # prints "count is now 1"
|
|
|
108
108
|
counter.doubled # => 2
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Signals are per-instance and created lazily; subclasses inherit all declarations. Use the block form for mutable defaults — a positional default is one object shared by every instance; see [Mutable state defaults](
|
|
111
|
+
Signals are per-instance and created lazily; subclasses inherit all declarations. Use the block form for mutable defaults — a positional default is one object shared by every instance; see [Mutable state defaults](https://planetaska.github.io/hibiki/mutable-defaults/) for the details.
|
|
112
112
|
|
|
113
113
|
## Documentation
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
Full documentation: <https://planetaska.github.io/hibiki/>
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
- [Guide](https://planetaska.github.io/hibiki/introduction/) — getting started, class-based reactivity, advanced usage
|
|
118
|
+
- [Rails](https://planetaska.github.io/hibiki/rails-introduction/) — the `hibiki_rails` and `hibiki_phlex` integration, generators, CRUD scaffolding
|
|
119
|
+
- [Reference](https://planetaska.github.io/hibiki/threading-model/) — threading model, lifecycle in detail, status and limitations
|
|
118
120
|
|
|
119
121
|
## Development
|
|
120
122
|
|
data/lib/hibiki/derived.rb
CHANGED
|
@@ -6,8 +6,12 @@ module Hibiki
|
|
|
6
6
|
include Trackable # observed by downstream deriveds/effects
|
|
7
7
|
include Observer # observes its own dependencies
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
# equals: per-signal equality (Solid's createMemo takes it too). A derived
|
|
10
|
+
# has no write gate, so it matters only at the flush gate — observers ask
|
|
11
|
+
# changed_from?, which consults it (see Trackable).
|
|
12
|
+
def initialize(equals: nil, &block)
|
|
10
13
|
@block = block
|
|
14
|
+
@equals = equals
|
|
11
15
|
@dirty = true
|
|
12
16
|
end
|
|
13
17
|
|
data/lib/hibiki/dsl.rb
CHANGED
|
@@ -5,8 +5,8 @@ module Hibiki
|
|
|
5
5
|
# Opt-in: `include Hibiki::DSL` where you want the bare helpers.
|
|
6
6
|
# The gem never includes it for you (no polluting Object/main).
|
|
7
7
|
module DSL
|
|
8
|
-
def state(value) = State.new(value)
|
|
9
|
-
def derived(&) = Derived.new(&)
|
|
8
|
+
def state(value, equals: nil) = State.new(value, equals:)
|
|
9
|
+
def derived(equals: nil, &) = Derived.new(equals:, &)
|
|
10
10
|
def effect(scheduler: nil, &) = Effect.new(scheduler:, &)
|
|
11
11
|
def batch(&) = Hibiki.batch(&)
|
|
12
12
|
def root(&) = Hibiki.root(&)
|
data/lib/hibiki/reactive.rb
CHANGED
|
@@ -34,16 +34,16 @@ module Hibiki
|
|
|
34
34
|
# instance, instance_exec'd, and untracked: first touch may happen
|
|
35
35
|
# inside some effect's tracking window, and a default that reads other
|
|
36
36
|
# signals must not subscribe that outer observer.
|
|
37
|
-
def state(name, default = nil, &default_block)
|
|
37
|
+
def state(name, default = nil, equals: nil, &default_block)
|
|
38
38
|
init = proc do
|
|
39
|
-
State.new(default_block ? Hibiki.untrack { instance_exec(&default_block) } : default)
|
|
39
|
+
State.new(default_block ? Hibiki.untrack { instance_exec(&default_block) } : default, equals:)
|
|
40
40
|
end
|
|
41
41
|
define_method(name) { __hibiki_signal(name, init).value }
|
|
42
42
|
define_method(:"#{name}=") { |new_value| __hibiki_signal(name, init).value = new_value }
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def derived(name, &)
|
|
46
|
-
init = proc { Derived.new { instance_exec(&) } }
|
|
45
|
+
def derived(name, equals: nil, &)
|
|
46
|
+
init = proc { Derived.new(equals:) { instance_exec(&) } }
|
|
47
47
|
define_method(name) { __hibiki_signal(name, init).value }
|
|
48
48
|
end
|
|
49
49
|
|
data/lib/hibiki/state.rb
CHANGED
|
@@ -5,8 +5,11 @@ module Hibiki
|
|
|
5
5
|
class State
|
|
6
6
|
include Trackable
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# equals: per-signal equality (Solid's createSignal(value, { equals })).
|
|
9
|
+
# nil → `==`; false → always notify; callable → comparator(prev, next).
|
|
10
|
+
def initialize(value, equals: nil)
|
|
9
11
|
@value = value
|
|
12
|
+
@equals = equals
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def value
|
|
@@ -22,7 +25,14 @@ module Hibiki
|
|
|
22
25
|
def call = value
|
|
23
26
|
|
|
24
27
|
def value=(new_value)
|
|
25
|
-
|
|
28
|
+
# The write gate: the signal's equality decides whether this write is a
|
|
29
|
+
# no-op. Its twin is the flush gate, Trackable#changed_from? — keep them
|
|
30
|
+
# answering the same way.
|
|
31
|
+
case @equals
|
|
32
|
+
when nil then return if new_value == @value
|
|
33
|
+
when false then nil # always notify
|
|
34
|
+
else return if @equals.call(@value, new_value)
|
|
35
|
+
end
|
|
26
36
|
|
|
27
37
|
@value = new_value
|
|
28
38
|
# Solid wraps every write in runUpdates; mirroring that, an unbatched
|
data/lib/hibiki/tracking.rb
CHANGED
|
@@ -76,6 +76,20 @@ module Hibiki
|
|
|
76
76
|
|
|
77
77
|
def unsubscribe(observer) = subscribers.delete(observer)
|
|
78
78
|
|
|
79
|
+
# The flush gate's question, answered by the source so its own equality
|
|
80
|
+
# decides (Solid's `equals` option, on createSignal and createMemo alike):
|
|
81
|
+
# nil → `==` as always, false → never equal (every write notifies),
|
|
82
|
+
# callable → comparator(prev, next). Both gates consult the same @equals —
|
|
83
|
+
# a comparator honored on write but not at flush would let the batch
|
|
84
|
+
# flush silently swallow the very change the write announced.
|
|
85
|
+
def changed_from?(seen)
|
|
86
|
+
case @equals
|
|
87
|
+
when nil then peek != seen
|
|
88
|
+
when false then true
|
|
89
|
+
else !@equals.call(seen, peek)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
79
93
|
def notify
|
|
80
94
|
# dup: invalidation may mutate the set while we iterate
|
|
81
95
|
subscribers.dup.each(&:invalidate)
|
|
@@ -102,15 +116,17 @@ module Hibiki
|
|
|
102
116
|
# The equality gate, asked at flush time (see Effect#invalidate): did any
|
|
103
117
|
# value we read actually change? Svelte's $derived compares; we compare on
|
|
104
118
|
# the observer's side instead of the producer's, because Derived#invalidate
|
|
105
|
-
# has already notified downstream by the time it knows its new value.
|
|
119
|
+
# has already notified downstream by the time it knows its new value. The
|
|
120
|
+
# comparison itself is delegated to each source (changed_from?), so a
|
|
121
|
+
# per-signal `equals:` is honored here too.
|
|
106
122
|
#
|
|
107
|
-
# `peek` resolves a dirty source without subscribing us to
|
|
108
|
-
# needed, since a Derived#recompute makes itself the
|
|
109
|
-
# short-circuits in the useful direction: sources are in
|
|
110
|
-
# changed first source spares the rest a validation
|
|
111
|
-
# recomputes them only if it reads them this time.
|
|
123
|
+
# changed_from?'s `peek` resolves a dirty source without subscribing us to
|
|
124
|
+
# it — no untrack needed, since a Derived#recompute makes itself the
|
|
125
|
+
# observer. `any?` short-circuits in the useful direction: sources are in
|
|
126
|
+
# read order, so a changed first source spares the rest a validation
|
|
127
|
+
# recompute, and the run recomputes them only if it reads them this time.
|
|
112
128
|
def sources_changed?
|
|
113
|
-
sources.any? { |source, seen| source.
|
|
129
|
+
sources.any? { |source, seen| source.changed_from?(seen) }
|
|
114
130
|
end
|
|
115
131
|
end
|
|
116
132
|
|
data/lib/hibiki/version.rb
CHANGED