diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb24bb..e6eddc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [1.2.0] - 2023-10-03 + +- Add `untracked` method (implements #5) +- Add `mutation_detected` check for `computed` + +Gem now roughly analogous to `@preact/signals-core` v1.5 + ## [1.1.0] - 2023-03-25 - Provide better signal/computed inspect strings (fixes #1) diff --git a/Gemfile.lock b/Gemfile.lock index 517981e..93ce23e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - signalize (1.1.0) + signalize (1.2.0) concurrent-ruby (~> 1.2) GEM diff --git a/README.md b/README.md index d2be88b..884f8b6 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,27 @@ counter = signal(0) counter.value += 1 ``` +### `untracked { }` + +In case when you're receiving a callback that can read some signals, but you don't want to subscribe to them, you can use `untracked` to prevent any subscriptions from happening. + +```ruby +require "signalize" +include Signalize::API + +counter = signal(0) +effect_count = signal(0) +fn = proc { effect_count.value + 1 } + +effect do + # Logs the value + puts counter.value + + # Whenever this effect is triggered, run `fn` that gives new value + effect_count.value = untracked(&fn) +end +``` + ### `computed { }` You derive computed state by accessing a signal's value within a `computed` block and returning a new value. Every time that signal value is updated, a computed value will likewise be updated. Actually, that's not quite accurate — the computed value only computes when it's read. In this sense, we can call computed values "lazily-evaluated". diff --git a/lib/signalize.rb b/lib/signalize.rb index 96a013b..e5a116e 100644 --- a/lib/signalize.rb +++ b/lib/signalize.rb @@ -21,6 +21,10 @@ def self.cycle_detected raise Signalize::Error, "Cycle detected" end + def self.mutation_detected + raise Signalize::Error, "Computed cannot have side-effects" + end + RUNNING = 1 << 0 NOTIFIED = 1 << 1 OUTDATED = 1 << 2 @@ -34,6 +38,10 @@ def self.cycle_detected global_map_accessor :eval_context self.eval_context = nil + # Used by `untracked` method + global_map_accessor :untracked_depth + self.untracked_depth = 0 + # Effects collected into a batch. global_map_accessor :batched_effect self.batched_effect = nil @@ -414,6 +422,8 @@ def value end def value=(value) + Signalize.mutation_detected if Signalize.eval_context.is_a?(Signalize::Computed) + if value != @value Signalize.cycle_detected if Signalize.batch_iteration > 100 @@ -484,7 +494,7 @@ def _refresh return true end - prevContext = Signalize.eval_context + prev_context = Signalize.eval_context begin Signalize.prepare_sources(self) Signalize.eval_context = self @@ -499,7 +509,7 @@ def _refresh @_flags |= HAS_ERROR @_version += 1 end - Signalize.eval_context = prevContext + Signalize.eval_context = prev_context Signalize.cleanup_sources(self) @_flags &= ~RUNNING @@ -667,6 +677,21 @@ def batch Signalize.end_batch end end + + def untracked + return yield unless Signalize.untracked_depth.zero? + + prev_context = Signalize.eval_context + Signalize.eval_context = nil + Signalize.untracked_depth += 1 + + begin + return yield + ensure + Signalize.untracked_depth -= 1 + Signalize.eval_context = prev_context + end + end end extend API diff --git a/lib/signalize/version.rb b/lib/signalize/version.rb index 177bec3..0011ef3 100644 --- a/lib/signalize/version.rb +++ b/lib/signalize/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Signalize - VERSION = "1.1.0" + VERSION = "1.2.0" end diff --git a/test/test_signalize.rb b/test/test_signalize.rb index 31a71ca..8e29273 100644 --- a/test/test_signalize.rb +++ b/test/test_signalize.rb @@ -146,4 +146,31 @@ def test_subscribe test_value = 10 counter.value = test_value # logs the new value end + + def test_disallow_setting_signal_in_computed + v = 123 + a = signal(v) + c = computed { a.value += 1 } + error = assert_raises(Signalize::Error) do + c.value + end + + assert_equal "Computed cannot have side-effects", error.message + assert_equal v, a.value + end + + def test_run_untracked_callback_once + calls = 0 + a = signal(1); + b = signal(2); + spy = proc do + calls += 1 + a.value + b.value + end + effect { untracked(&spy) } + a.value = 10 + b.value = 20 + + assert_equal 1, calls + end end