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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d473629248429556ab9410edc059933072d1a6c6f462fd9fa225c25684d45c79
4
- data.tar.gz: 025ac97891ad5933eecd82201323f3c329e3dedc392f6407e6004175ee7c316a
3
+ metadata.gz: 62668f45023a6d59c07b6f414b0c6ec50a7ff264b5dc9d2b6ad0bedfec9510cc
4
+ data.tar.gz: 9391c0a9b3865c61a0c6cc7f2463e3a377cac91b89820fd3cb3c34dffcc52ddc
5
5
  SHA512:
6
- metadata.gz: 897c70917201523c25cd459fe84ee402cd25a92268729c505fff5109bf4f51e462f69791cc4f3204343d06193983d1f50498d5e0ea6dfadeffd0260721f6fbbb
7
- data.tar.gz: 693c6e0cae17d37c3c1b1eeea8b94ee5323ecdf103f89690c34389779c37f426fd49fc4141640e6666ba3d8f1bbd78856a1b6399ec92a166dca307daa8269c59
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](docs-md/mutable-defaults.md) for the details.
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
- Documentation site: <https://planetaska.github.io/hibiki/>
115
+ Full documentation: <https://planetaska.github.io/hibiki/>
116
116
 
117
- More detail in plain markdown: [docs-md/](docs-md/)
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
 
@@ -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
- def initialize(&block)
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(&)
@@ -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
- def initialize(value)
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
- return if new_value == @value
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
@@ -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 it — no untrack
108
- # needed, since a Derived#recompute makes itself the observer. `any?`
109
- # short-circuits in the useful direction: sources are in read order, so a
110
- # changed first source spares the rest a validation recompute, and the run
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.peek != seen }
129
+ sources.any? { |source, seen| source.changed_from?(seen) }
114
130
  end
115
131
  end
116
132
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hibiki
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hibiki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - planetaska