phlex-reactive 0.16.1 → 0.16.2

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: 860b397593c4d0a2d05d2485345bfc78564e784942c8413db1b67fbbacea6b41
4
- data.tar.gz: '059b1148284859d6a69afdd4f62693f4dbedb0eab41370af843e30a4f3d27e4f'
3
+ metadata.gz: 96630c84c36efda322bafa8792e9e00bc0827d727a8d9db9c0f3191916f3a561
4
+ data.tar.gz: 2fc0570f0888b0905f04573b0ee8ecc71210ad08e5e10f430067b7141d1c468f
5
5
  SHA512:
6
- metadata.gz: 0500ccbf1d82ea1039cf078dbe62e67efd6da9c9a2ecbeaace46d7d316f4a6c62ef479ea8b1a81e263ad1f738c25d9aa64f0dc53fc7af7a1bbadd6dab49e5021
7
- data.tar.gz: eae4009ae39fba74bc6219c4b90df31589bed31ed70a457c7f240075ffd50b84662a39af475d878bc339d2e538611e248a287e11a5bbdacd52cb47d1bf73d45e
6
+ metadata.gz: c8487e66e0f93336442b6edbaaaa2cd2afdf0e18c809783e9001f25b1aaaf7525baab624c8d6b20a3acc896ccb1213423123d4ef914d335dcd147a4b19ab4927
7
+ data.tar.gz: 02d982c7a73c156d52d5a707e1745ea4de5df759f8d7c9d402c51c1ddabd68800d12a8972bf18fe9f5d10e074844922c6b31eec228508ebd14acb8ccac85d58b
data/CHANGELOG.md CHANGED
@@ -6,6 +6,39 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.16.2] - 2026-10-17
10
+
11
+ ### Fixed
12
+
13
+ - **An app on Rails main boots in Ractor mode with phlex-reactive mounted.**
14
+ Rails main's `RouteSet#finalize!` makes the route set Ractor-shareable, the
15
+ blocks engines append included. The engine's route block had the engine as
16
+ its `self`, so with `ActiveSupport::Ractors.unshareable_proc_action = :raise`
17
+ boot raised `Ractor::IsolationError: Proc's self is not shareable`. On Ruby
18
+ 4.0+ the block is now built with `Ractor.shareable_proc`; the routes it
19
+ draws are unchanged, and older Rubies get the same block as before.
20
+
21
+ - **A warmed component's class can be read from a non-main Ractor.** Boot's
22
+ `Component::Registry.warm!` froze each class's resolution cache only at the
23
+ top level, so the action definitions in it (their `params` Hash, their
24
+ compiled `ParamSchema`) stayed mutable, and the class's own declarations
25
+ (`@reactive_own_actions`, `@reactive_own_state_keys`, …) were not frozen at
26
+ all. Every reactive render or action in a non-main Ractor raised
27
+ `Ractor::IsolationError: can not get unshareable values from instance
28
+ variables of classes/modules`. warm! now shares the cache, every
29
+ `@reactive_own_*` declaration and the render memos deeply
30
+ (`Phlex::Reactive::Shareable.share`, which learned to copy `Data` values
31
+ that keep Data's own `initialize`; one with its own is kept as it is);
32
+ a compiled `ParamSchema` is frozen with a deep-frozen copy of its schema,
33
+ and `action` keeps a frozen copy of `params:`, so the app's own Hash is
34
+ never frozen. A value no Ractor can share (a `reactive_collection size:`
35
+ lambda that assigns what it captures) is kept as it is and works on the
36
+ main Ractor; boot never raises for it. A declaration after warm! (a
37
+ development reload) writes to a fresh copy, as before. The Ractor canary
38
+ now reads a warmed component with an action that takes params.
39
+
40
+ ## [0.16.1] - 2026-10-17
41
+
9
42
  ### Added
10
43
 
11
44
  - **Draggable regions: move and resize a box, or move a point, inside a
@@ -4114,7 +4147,9 @@ room for dogfood fixes.
4114
4147
  scaffolds a reactive component (and an RSpec spec when the app uses RSpec),
4115
4148
  state-backed by default or record-backed with `--record`.
4116
4149
 
4117
- [Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.0...HEAD
4150
+ [Unreleased]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.2...HEAD
4151
+ [0.16.2]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.1...v0.16.2
4152
+ [0.16.1]: https://github.com/mhenrixon/phlex-reactive/compare/v0.16.0...v0.16.1
4118
4153
  [0.16.0]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.5...v0.16.0
4119
4154
  [0.15.5]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.4...v0.15.5
4120
4155
  [0.15.4]: https://github.com/mhenrixon/phlex-reactive/compare/v0.15.3...v0.15.4
@@ -311,6 +311,9 @@ module Phlex
311
311
 
312
312
  # Issue #184: params: :symbol resolves a registered named schema.
313
313
  params = Phlex::Reactive.param_schema(params) if params.is_a?(Symbol)
314
+ # A deep-frozen copy, shared with the compiled schema below: the
315
+ # definition stays readable from a non-main Ractor once warmed.
316
+ params = Phlex::Reactive::Shareable.share(params)
314
317
  # If a scope is already declared, reject a schema nested under the scope
315
318
  # key here (the scope-first declaration order). The action-first order is
316
319
  # caught in reactive_scope.
@@ -103,6 +103,13 @@ module Phlex
103
103
  # attribute fragments), shared by warm! so a non-main Ractor can read it.
104
104
  MEMO_IVARS = %i[@reactive_record_ivar @reactive_state_ivars @reactive_effect_attrs @reactive_on_complete_attr].freeze
105
105
 
106
+ # Every class ivar warm! leaves for a request to read, each shared
107
+ # deeply so a non-main Ractor can read it: the class's own
108
+ # declarations, the resolution cache and the render path's memos.
109
+ # (The generation counters next to them are Integers, shareable as
110
+ # they are.)
111
+ WARMED_IVARS = [*OWN_IVARS.values, :@reactive_registry_cache, *MEMO_IVARS].freeze
112
+
106
113
  # Every class with the reactive DSL, a ClientBindings-only view
107
114
  # included (Streamable's registry has only the token-bearing ones):
108
115
  # what the engine warms at boot. Per-Ractor (Phlex::Reactive::Store);
@@ -161,7 +168,7 @@ module Phlex
161
168
 
162
169
  # Resolve every registry kind and build the render path's per-class
163
170
  # memos for each reactive component class among `classes`, then
164
- # freeze each class's resolution cache. The engine runs it at boot
171
+ # share (deep-freeze) what each class keeps (WARMED_IVARS). The engine runs it at boot
165
172
  # when the app eager-loads (every class is loaded by then), so a
166
173
  # request reads memos and never writes the class. A declaration
167
174
  # after it (a development reload) bumps the generation and the next
@@ -226,10 +233,14 @@ module Phlex
226
233
  end
227
234
 
228
235
  # The class's own storage for `kind`, initialized from the block on
229
- # first write.
236
+ # first write. warm! shares (freezes) it, so a declaration after
237
+ # warm! (a development reload) writes to a fresh copy.
230
238
  def own!(klass, kind)
231
239
  ivar = OWN_IVARS.fetch(kind)
232
- klass.instance_variable_get(ivar) || klass.instance_variable_set(ivar, yield)
240
+ current = klass.instance_variable_get(ivar)
241
+ return current if current && !current.frozen?
242
+
243
+ klass.instance_variable_set(ivar, current ? current.dup : yield)
233
244
  end
234
245
 
235
246
  # Invalidate every resolution memo in the process (generation bump)
@@ -257,8 +268,7 @@ module Phlex
257
268
 
258
269
  def warm_class(klass)
259
270
  [*READERS.values, *MEMO_READERS].each { klass.public_send(it) if klass.respond_to?(it) }
260
- klass.instance_variable_get(:@reactive_registry_cache)&.freeze
261
- MEMO_IVARS.each do
271
+ WARMED_IVARS.each do
262
272
  next unless klass.instance_variable_defined?(it)
263
273
 
264
274
  klass.instance_variable_set(it, Phlex::Reactive::Shareable.share(klass.instance_variable_get(it)))
@@ -34,13 +34,20 @@ module Phlex
34
34
  # (the privately cacheable lazy render, issue #277). Apps can change the
35
35
  # paths with Phlex::Reactive.action_path / .defer_path / .fragment_path
36
36
  # before boot.
37
+ #
38
+ # Rails main's RouteSet#finalize! makes the route set Ractor-shareable,
39
+ # append blocks included, and a block's self here is the engine, which
40
+ # no Ractor can share. Ractor.shareable_proc (Ruby 4.0+) gives the block
41
+ # a nil self; Mapper evaluates it with its own self either way.
37
42
  initializer "phlex_reactive.routes" do
38
- it.routes.append do
43
+ routes = proc do
39
44
  post Phlex::Reactive.action_path, to: "phlex/reactive/actions#create", as: :phlex_reactive_action
40
45
  post Phlex::Reactive.defer_path, to: "phlex/reactive/actions#deferred", as: :phlex_reactive_defer
41
46
  get "#{Phlex::Reactive.fragment_path}/:id", to: "phlex/reactive/fragments#show",
42
47
  as: :phlex_reactive_fragment
43
48
  end
49
+ routes = ::Ractor.shareable_proc(&routes) if defined?(::Ractor) && ::Ractor.respond_to?(:shareable_proc)
50
+ it.routes.append(&routes)
44
51
  end
45
52
 
46
53
  # Make the MINIFIED client build available to Propshaft/Sprockets so it can
@@ -143,8 +143,12 @@ module Phlex
143
143
 
144
144
  attr_reader :schema
145
145
 
146
+ # Ractor-shareable once built: a deep-frozen copy of the declared schema
147
+ # (the app's own Hash is left alone) and nothing written after this, so
148
+ # a warmed action definition can be read from a non-main Ractor.
146
149
  def initialize(schema)
147
- @schema = schema
150
+ @schema = Phlex::Reactive::Shareable.share(schema)
151
+ freeze
148
152
  end
149
153
 
150
154
  # Coerce client params against this schema. Anything not in the schema is
@@ -23,6 +23,10 @@ module Phlex
23
23
  # works on the main Ractor only;
24
24
  # * a Hash keeps its default value or default proc and its
25
25
  # compare_by_identity;
26
+ # * a Data value (the gem's declarations: an action, a compute, a
27
+ # collection) comes back as a copy with every member shared, when
28
+ # its class keeps Data's own initialize (one of its own may
29
+ # normalize or validate, and is never run again);
26
30
  # * any other object (a MessageVerifier, an APM adapter, an app object)
27
31
  # is kept as is.
28
32
  #
@@ -35,6 +39,7 @@ module Phlex
35
39
  when ::Proc then share_proc(value.dup) || value
36
40
  when ::Array then seal(value.map { share(it) })
37
41
  when ::Hash then share_hash(value)
42
+ when ::Data then plain_data?(value) ? share_data(value) : value
38
43
  when ::String then value.dup.freeze
39
44
  else value
40
45
  end
@@ -60,12 +65,14 @@ module Phlex
60
65
  end
61
66
 
62
67
  # The Proc made shareable in place, or nil when it cannot be (or there
63
- # is no Ractor).
68
+ # is no Ractor). Ruby raises Ractor::IsolationError for an unshareable
69
+ # self or capture, and ArgumentError for a Proc that assigns a variable
70
+ # it captures.
64
71
  def share_proc(callable)
65
72
  return unless defined?(::Ractor)
66
73
 
67
74
  ::Ractor.make_shareable(callable)
68
- rescue ::Ractor::Error
75
+ rescue ::Ractor::Error, ::ArgumentError
69
76
  nil
70
77
  end
71
78
 
@@ -78,10 +85,18 @@ module Phlex
78
85
  seal(copy)
79
86
  end
80
87
 
88
+ # Data#with runs the class's initialize again: only Data's own, which
89
+ # just assigns the members, is safe to.
90
+ def plain_data?(data) = data.class.instance_method(:initialize).owner.equal?(::Data)
91
+
92
+ # A copy with every member shared. Data is always frozen, so the copy
93
+ # is shareable when every member is.
94
+ def share_data(data) = data.with(**data.to_h.transform_values { share(it) })
95
+
81
96
  # Frozen; shareable as well when everything inside it is.
82
97
  def seal(collection) = collection.freeze
83
98
 
84
- private_class_method :share_proc, :share_hash, :seal
99
+ private_class_method :share_proc, :share_hash, :plain_data?, :share_data, :seal
85
100
  end
86
101
  end
87
102
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.16.1"
5
+ VERSION = "0.16.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.1
4
+ version: 0.16.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson