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 +4 -4
- data/CHANGELOG.md +36 -1
- data/lib/phlex/reactive/component/dsl.rb +3 -0
- data/lib/phlex/reactive/component/registry.rb +15 -5
- data/lib/phlex/reactive/engine.rb +8 -1
- data/lib/phlex/reactive/param_schema.rb +5 -1
- data/lib/phlex/reactive/shareable.rb +18 -3
- data/lib/phlex/reactive/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: 96630c84c36efda322bafa8792e9e00bc0827d727a8d9db9c0f3191916f3a561
|
|
4
|
+
data.tar.gz: 2fc0570f0888b0905f04573b0ee8ecc71210ad08e5e10f430067b7141d1c468f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|