phlex-reactive 0.15.5 → 0.16.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 +100 -0
- data/README.md +15 -2
- data/app/controllers/phlex/reactive/fragments_controller.rb +4 -3
- data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +2 -1
- data/lib/phlex/reactive/apm/subscriber.rb +5 -0
- data/lib/phlex/reactive/apm.rb +5 -6
- data/lib/phlex/reactive/authorization.rb +31 -5
- data/lib/phlex/reactive/client_bindings.rb +19 -0
- data/lib/phlex/reactive/component/dsl.rb +15 -1
- data/lib/phlex/reactive/component/helpers.rb +4 -0
- data/lib/phlex/reactive/component/identity.rb +17 -6
- data/lib/phlex/reactive/component/lazy.rb +4 -0
- data/lib/phlex/reactive/component/presence_declaration.rb +4 -0
- data/lib/phlex/reactive/component/registry.rb +76 -6
- data/lib/phlex/reactive/component/sortable.rb +3 -0
- data/lib/phlex/reactive/component.rb +4 -0
- data/lib/phlex/reactive/defer.rb +4 -4
- data/lib/phlex/reactive/doctor/ractor_readiness.rb +65 -0
- data/lib/phlex/reactive/doctor.rb +2 -0
- data/lib/phlex/reactive/engine.rb +25 -0
- data/lib/phlex/reactive/fragment.rb +5 -4
- data/lib/phlex/reactive/param_schema.rb +3 -2
- data/lib/phlex/reactive/pending/markup.rb +27 -9
- data/lib/phlex/reactive/pending.rb +4 -4
- data/lib/phlex/reactive/shareable.rb +87 -0
- data/lib/phlex/reactive/store.rb +73 -0
- data/lib/phlex/reactive/streamable.rb +49 -19
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +293 -122
- data/sig/phlex/reactive/component.rbs +108 -0
- data/sig/phlex/reactive/param_schema.rbs +23 -0
- data/sig/phlex/reactive/response.rbs +70 -0
- data/sig/phlex/reactive/streamable.rbs +62 -0
- data/sig/phlex/reactive.rbs +231 -0
- metadata +9 -1
|
@@ -17,6 +17,7 @@ module Phlex
|
|
|
17
17
|
# trip and (for the component checks) iterate the loaded Streamable registry.
|
|
18
18
|
class Doctor
|
|
19
19
|
include FiberIsolation
|
|
20
|
+
include RactorReadiness
|
|
20
21
|
|
|
21
22
|
# The result of one check: a status (:ok/:fail/:unknown), a human message,
|
|
22
23
|
# and (on anything but :ok) a fix line telling the adopter what to do. A
|
|
@@ -79,6 +80,7 @@ module Phlex
|
|
|
79
80
|
verifier_check,
|
|
80
81
|
base_controller_check,
|
|
81
82
|
fiber_isolation_check,
|
|
83
|
+
ractor_readiness_check,
|
|
82
84
|
action_check(components),
|
|
83
85
|
id_check(components),
|
|
84
86
|
authorization_check(components),
|
|
@@ -208,6 +208,31 @@ module Phlex
|
|
|
208
208
|
# app on Action Cable boots exactly as before.
|
|
209
209
|
Phlex::Reactive::Presence.install!
|
|
210
210
|
end
|
|
211
|
+
|
|
212
|
+
# Finalize the configuration once boot is over (zoolutions/phlex-reactive#364).
|
|
213
|
+
# The after_initialize block is registered from an initializer that runs
|
|
214
|
+
# after the app's config/initializers, so it runs after every
|
|
215
|
+
# after_initialize block the app registered there or in
|
|
216
|
+
# config/application.rb: one that appends to authorization_errors (the
|
|
217
|
+
# documented pattern for an app's own constants) still finds the list open.
|
|
218
|
+
initializer "phlex_reactive.finalize", after: :load_config_initializers do
|
|
219
|
+
config.after_initialize { Phlex::Reactive::Engine.finalize_boot! }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# What the finalize hook runs; public so a spec can drive it. With eager
|
|
223
|
+
# loading every component class is loaded by now, so verify_authorized's
|
|
224
|
+
# interceptor is prepended to every token-bearing class, and every
|
|
225
|
+
# class's registries and render memos (a client-only view's too) are
|
|
226
|
+
# resolved here, at boot, rather than by the first request for each
|
|
227
|
+
# class (Authorization.instrument_all!, Component::Registry.warm!).
|
|
228
|
+
def self.finalize_boot!(eager_load: ::Rails.application.config.eager_load)
|
|
229
|
+
registry = Phlex::Reactive::Component::Registry
|
|
230
|
+
if Phlex::Reactive.verify_authorized
|
|
231
|
+
Phlex::Reactive::Authorization.instrument_all!(eager_load ? Phlex::Reactive::Streamable.registered_classes : [])
|
|
232
|
+
end
|
|
233
|
+
Phlex::Reactive.finalize!
|
|
234
|
+
registry.warm!(eager_load ? registry.registered_classes : [])
|
|
235
|
+
end
|
|
211
236
|
end
|
|
212
237
|
end
|
|
213
238
|
end
|
|
@@ -41,8 +41,9 @@ module Phlex
|
|
|
41
41
|
# a collection is walked, so no value can make the shell render slow.
|
|
42
42
|
VIEWER_PART_LIMIT = 32
|
|
43
43
|
VIEWER_NODE_LIMIT = 64
|
|
44
|
-
#
|
|
45
|
-
|
|
44
|
+
# The per-Ractor table (Phlex::Reactive::Store) of the components already
|
|
45
|
+
# warned about, by class name: once per component.
|
|
46
|
+
VIEWER_WARNINGS_KEY = :phlex_reactive_fragment_viewer_warnings
|
|
46
47
|
|
|
47
48
|
# A reactive_cache_viewer value that cannot name a viewer. Internal: it
|
|
48
49
|
# carries the reason from the walk to viewer_param, which logs it.
|
|
@@ -207,7 +208,7 @@ module Phlex
|
|
|
207
208
|
# component's hook — and answers nil: "this value names nobody".
|
|
208
209
|
def warn_unusable_viewer(owner, reason)
|
|
209
210
|
name = owner.respond_to?(:name) ? owner.name.to_s : owner.to_s
|
|
210
|
-
return nil
|
|
211
|
+
return nil unless Phlex::Reactive::Store.table(VIEWER_WARNINGS_KEY).claim?(name)
|
|
211
212
|
return nil unless defined?(::Rails) && ::Rails.respond_to?(:logger)
|
|
212
213
|
|
|
213
214
|
::Rails.logger&.warn(
|
|
@@ -220,7 +221,7 @@ module Phlex
|
|
|
220
221
|
|
|
221
222
|
# Test seam: let the once-per-component warnings fire again.
|
|
222
223
|
def reset_viewer_warning!
|
|
223
|
-
|
|
224
|
+
Phlex::Reactive::Store.table(VIEWER_WARNINGS_KEY).clear
|
|
224
225
|
end
|
|
225
226
|
|
|
226
227
|
# The max-age (seconds) the endpoint answers with: the component's
|
|
@@ -136,8 +136,9 @@ module Phlex
|
|
|
136
136
|
|
|
137
137
|
# ActiveModel's Boolean type, instantiated once — the exact caster the
|
|
138
138
|
# controller used (an unrecognized value casts to nil, which is KEPT, not
|
|
139
|
-
# dropped).
|
|
140
|
-
|
|
139
|
+
# dropped). Ractor-shareable (it holds no state a cast writes), so a
|
|
140
|
+
# non-main Ractor can coerce through it (zoolutions/phlex-reactive#364).
|
|
141
|
+
BOOLEAN_TYPE = defined?(::Ractor) ? ::Ractor.make_shareable(ActiveModel::Type::Boolean.new) : ActiveModel::Type::Boolean.new
|
|
141
142
|
private_constant :BOOLEAN_TYPE
|
|
142
143
|
|
|
143
144
|
attr_reader :schema
|
|
@@ -49,11 +49,28 @@ module Phlex
|
|
|
49
49
|
# the first `>`.
|
|
50
50
|
ROOT_TAG = %r{\A\s*<[^\s>/]+(?:\s+[^\s=>/]+(?:="[^"]*")?)*}
|
|
51
51
|
|
|
52
|
-
#
|
|
53
|
-
# pinned between resets. A WeakKeyMap, not a WeakMap:
|
|
54
|
-
# the variant, so a weak VALUE would let GC drop the
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
# The variant memo, keyed weakly on the row class, so a class Zeitwerk
|
|
53
|
+
# replaced is not pinned between resets. A WeakKeyMap, not a WeakMap:
|
|
54
|
+
# nothing else holds the variant, so a weak VALUE would let GC drop the
|
|
55
|
+
# memo. Per-Ractor state (Phlex::Reactive::Store,
|
|
56
|
+
# zoolutions/phlex-reactive#364): a WeakKeyMap and its Mutex in module
|
|
57
|
+
# ivars could not be read from any other Ractor.
|
|
58
|
+
class Variants
|
|
59
|
+
def initialize
|
|
60
|
+
@mutex = Mutex.new
|
|
61
|
+
@map = ObjectSpace::WeakKeyMap.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fetch(row_class)
|
|
65
|
+
@mutex.synchronize { @map[row_class] ||= yield }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def reset!
|
|
69
|
+
@mutex.synchronize { @map = ObjectSpace::WeakKeyMap.new }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
VARIANTS_KEY = :phlex_reactive_pending_variants
|
|
57
74
|
|
|
58
75
|
class << self
|
|
59
76
|
# The row's pending markup as HTML, or nil when the row has no hook
|
|
@@ -78,13 +95,14 @@ module Phlex
|
|
|
78
95
|
end
|
|
79
96
|
|
|
80
97
|
def variant_for(row_class)
|
|
81
|
-
|
|
98
|
+
variants.fetch(row_class) { build_variant(row_class) }
|
|
82
99
|
end
|
|
83
100
|
|
|
84
101
|
# Called from the engine's config.to_prepare (Rails code reload).
|
|
85
|
-
def reset!
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
def reset! = variants.reset!
|
|
103
|
+
|
|
104
|
+
# This Ractor's variant memo.
|
|
105
|
+
def variants = Phlex::Reactive::Store.fetch(VARIANTS_KEY) { Variants.new }
|
|
88
106
|
|
|
89
107
|
private
|
|
90
108
|
|
|
@@ -322,11 +322,11 @@ data-reactive-defer-src="#{ERB::Util.html_escape(src)}" data-reactive-defer-sinc
|
|
|
322
322
|
|
|
323
323
|
# No settle lane is a CONFIG fact, not a per-reply event — warn once per
|
|
324
324
|
# process (a pending reply can fire per click; per-reply spam would bury
|
|
325
|
-
# the signal), then degrade to a plain enqueue.
|
|
325
|
+
# the signal), then degrade to a plain enqueue. The "warned" flag is
|
|
326
|
+
# per-Ractor state (Store), never a module ivar written on the request
|
|
327
|
+
# path.
|
|
326
328
|
def warn_no_lane
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
@no_lane_warned = true
|
|
329
|
+
return unless Phlex::Reactive::Store.first_warning?(:pending_no_lane)
|
|
330
330
|
return unless defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
|
331
331
|
|
|
332
332
|
::Rails.logger.warn(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Reactive
|
|
5
|
+
# Ractor-shareable values for the state the gem keeps on its modules and
|
|
6
|
+
# classes (zoolutions/phlex-reactive#364): a non-main Ractor can read a
|
|
7
|
+
# module instance variable or a constant only when its value is
|
|
8
|
+
# Ractor-shareable.
|
|
9
|
+
module Shareable
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# A Ractor-shareable version of value when one can be made, else value
|
|
13
|
+
# itself (frozen when it is a collection). Never freezes or copies
|
|
14
|
+
# anything the caller still holds behind its back:
|
|
15
|
+
#
|
|
16
|
+
# * plain data (Strings, Arrays, Hashes, Symbols, Integers, classes...)
|
|
17
|
+
# comes back as a deep-frozen COPY, so the app's own objects stay
|
|
18
|
+
# mutable;
|
|
19
|
+
# * a Proc comes back as a shareable COPY, which works only when its
|
|
20
|
+
# self and everything it captures are shareable already (a lambda
|
|
21
|
+
# written in a module body). Ruby never freezes what a Proc captures:
|
|
22
|
+
# a block from an initializer (self is `main`) is kept as is, and
|
|
23
|
+
# works on the main Ractor only;
|
|
24
|
+
# * a Hash keeps its default value or default proc and its
|
|
25
|
+
# compare_by_identity;
|
|
26
|
+
# * any other object (a MessageVerifier, an APM adapter, an app object)
|
|
27
|
+
# is kept as is.
|
|
28
|
+
#
|
|
29
|
+
# Engines without Ractor get the value back unchanged.
|
|
30
|
+
def share(value)
|
|
31
|
+
return value unless defined?(::Ractor)
|
|
32
|
+
return value if ::Ractor.shareable?(value)
|
|
33
|
+
|
|
34
|
+
case value
|
|
35
|
+
when ::Proc then share_proc(value.dup) || value
|
|
36
|
+
when ::Array then seal(value.map { share(it) })
|
|
37
|
+
when ::Hash then share_hash(value)
|
|
38
|
+
when ::String then value.dup.freeze
|
|
39
|
+
else value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Is value shareable (and so readable from any Ractor)? True on an engine
|
|
44
|
+
# without Ractor, where there is only one.
|
|
45
|
+
def shareable?(value) = defined?(::Ractor) ? ::Ractor.shareable?(value) : true
|
|
46
|
+
|
|
47
|
+
# ActiveSupport::Concern keeps its bookkeeping in instance variables of
|
|
48
|
+
# the concern module itself: @_dependencies (the concerns it includes)
|
|
49
|
+
# and the included/prepended block. They are written while the module
|
|
50
|
+
# body loads and only read when a class includes the concern, so they
|
|
51
|
+
# are sealed at the end of the body: the list frozen, the block (whose
|
|
52
|
+
# self is the module, capturing nothing) made shareable.
|
|
53
|
+
def seal_concern(mod)
|
|
54
|
+
dependencies = mod.instance_variable_get(:@_dependencies) if mod.instance_variable_defined?(:@_dependencies)
|
|
55
|
+
dependencies&.freeze
|
|
56
|
+
%i[@_included_block @_prepended_block].each do
|
|
57
|
+
share_proc(mod.instance_variable_get(it)) if mod.instance_variable_defined?(it)
|
|
58
|
+
end
|
|
59
|
+
mod
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The Proc made shareable in place, or nil when it cannot be (or there
|
|
63
|
+
# is no Ractor).
|
|
64
|
+
def share_proc(callable)
|
|
65
|
+
return unless defined?(::Ractor)
|
|
66
|
+
|
|
67
|
+
::Ractor.make_shareable(callable)
|
|
68
|
+
rescue ::Ractor::Error
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A plain Hash copy with every key and value shared, keeping the
|
|
73
|
+
# default (value or proc) and compare_by_identity.
|
|
74
|
+
def share_hash(hash)
|
|
75
|
+
copy = hash.compare_by_identity? ? {}.compare_by_identity : {}
|
|
76
|
+
hash.default_proc ? copy.default_proc = share(hash.default_proc) : copy.default = share(hash.default)
|
|
77
|
+
hash.each { |key, value| copy[share(key)] = share(value) }
|
|
78
|
+
seal(copy)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Frozen; shareable as well when everything inside it is.
|
|
82
|
+
def seal(collection) = collection.freeze
|
|
83
|
+
|
|
84
|
+
private_class_method :share_proc, :share_hash, :seal
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Reactive
|
|
5
|
+
# Per-Ractor lazy state (zoolutions/phlex-reactive#364): the home for a
|
|
6
|
+
# process cache that must not live in a constant or a class instance
|
|
7
|
+
# variable, where a non-main Ractor could not read it (a Mutex or a
|
|
8
|
+
# mutable Hash is never Ractor-shareable) and never write it.
|
|
9
|
+
#
|
|
10
|
+
# Store.fetch(:phlex_reactive_thing) { build } # one value per Ractor
|
|
11
|
+
# Store.table(:phlex_reactive_memo).fetch(key) { compute }
|
|
12
|
+
#
|
|
13
|
+
# On an engine with Ractor.store_if_absent (MRI 3.4+, so every Ruby
|
|
14
|
+
# phlex-reactive supports there) this is that: atomic, one value per
|
|
15
|
+
# Ractor. An engine without Ractors (JRuby, TruffleRuby) has only one, so
|
|
16
|
+
# one Mutex-guarded FALLBACK table stands in. Keys are Symbols prefixed
|
|
17
|
+
# with the gem's name. The pattern is conformance-kit's
|
|
18
|
+
# ConformanceKit::Store, copied here because the kit is a development
|
|
19
|
+
# dependency only.
|
|
20
|
+
module Store
|
|
21
|
+
# A Hash guarded by its own Mutex: the per-Ractor memo the threads of one
|
|
22
|
+
# Ractor share. Built inside the Ractor that uses it, so the lock is
|
|
23
|
+
# never shared across Ractors.
|
|
24
|
+
class Table
|
|
25
|
+
def initialize
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@values = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The value under key, computed once with the block when absent.
|
|
31
|
+
def fetch(key)
|
|
32
|
+
@mutex.synchronize { @values.fetch(key) { @values[key] = yield } }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# True the first time key is claimed (until #clear), false after: a
|
|
36
|
+
# once-per-Ractor guard, e.g. for a warning.
|
|
37
|
+
def claim?(key)
|
|
38
|
+
@mutex.synchronize { @values.key?(key) ? false : (@values[key] = true) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear
|
|
42
|
+
@mutex.synchronize { @values.clear }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
NATIVE = defined?(::Ractor) ? ::Ractor.respond_to?(:store_if_absent) : false
|
|
47
|
+
# Only an engine without Ractor.store_if_absent builds it: on MRI the
|
|
48
|
+
# module holds no lock and no mutable state at all.
|
|
49
|
+
FALLBACK = (Table.new unless NATIVE)
|
|
50
|
+
|
|
51
|
+
# The table of the once-per-process config warnings (Defer, Pending):
|
|
52
|
+
# once per Ractor, which in practice is the main one.
|
|
53
|
+
WARNINGS_KEY = :phlex_reactive_warnings
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def fetch(key, &init)
|
|
57
|
+
raise ::ArgumentError, "Phlex::Reactive::Store.fetch(#{key.inspect}) needs an initializer block" unless init
|
|
58
|
+
return ::Ractor.store_if_absent(key, &init) if native?
|
|
59
|
+
|
|
60
|
+
FALLBACK.fetch(key, &init)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The current Ractor's Table under key.
|
|
64
|
+
def table(key) = fetch(key) { Table.new }
|
|
65
|
+
|
|
66
|
+
# True the first time the named warning is due in this Ractor.
|
|
67
|
+
def first_warning?(name) = table(WARNINGS_KEY).claim?(name)
|
|
68
|
+
|
|
69
|
+
def native? = NATIVE
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -97,22 +97,53 @@ module Phlex
|
|
|
97
97
|
# WeakMap used as a set (the class is the key) so a class reloaded by
|
|
98
98
|
# Zeitwerk in dev is GC'd normally — a strong Array would pin every
|
|
99
99
|
# reloaded generation and leak across reloads. Guarded by a mutex;
|
|
100
|
-
# registration happens at class-load time
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
# registration happens at class-load time.
|
|
101
|
+
#
|
|
102
|
+
# Per-Ractor state (Phlex::Reactive::Store, zoolutions/phlex-reactive#364):
|
|
103
|
+
# a WeakMap and a Mutex are never Ractor-shareable, so in a module ivar no
|
|
104
|
+
# other Ractor could read them. Classes are defined on the main Ractor
|
|
105
|
+
# (boot, a development reload), so in practice this is one registry.
|
|
106
|
+
class ClassRegistry
|
|
107
|
+
def initialize
|
|
108
|
+
@mutex = Mutex.new
|
|
109
|
+
@classes = ObjectSpace::WeakMap.new
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add(klass)
|
|
113
|
+
@mutex.synchronize { @classes[klass] = true }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A point-in-time snapshot, taken under the lock.
|
|
117
|
+
def to_a
|
|
118
|
+
@mutex.synchronize do
|
|
119
|
+
classes = []
|
|
120
|
+
@classes.each_key { classes << it }
|
|
121
|
+
classes
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
REGISTRY_KEY = :phlex_reactive_streamable_registry
|
|
103
127
|
|
|
104
128
|
class << self
|
|
129
|
+
# This Ractor's ClassRegistry.
|
|
130
|
+
def class_registry = Phlex::Reactive::Store.fetch(REGISTRY_KEY) { ClassRegistry.new }
|
|
131
|
+
|
|
105
132
|
# Reset every streamable class's cached view context + builder. Called
|
|
106
133
|
# from the engine's reloader (config.to_prepare) so a reloaded renderer
|
|
107
134
|
# controller is never served from a stale memo. No-op outside Rails.
|
|
108
135
|
def reset_all_view_contexts!
|
|
109
|
-
|
|
110
|
-
@registry.each_key(&:reset_turbo_view_context!)
|
|
111
|
-
end
|
|
136
|
+
class_registry.to_a.each(&:reset_turbo_view_context!)
|
|
112
137
|
end
|
|
113
138
|
|
|
139
|
+
# Also starts the class's view-context generation at 0 here, when the
|
|
140
|
+
# class is defined (boot, or a development reload, on the main Ractor),
|
|
141
|
+
# so reading it on the render path never writes the class
|
|
142
|
+
# (zoolutions/phlex-reactive#364).
|
|
114
143
|
def register(klass)
|
|
115
|
-
|
|
144
|
+
klass.instance_variable_set(:@turbo_view_context_generation, 0) unless
|
|
145
|
+
klass.instance_variable_defined?(:@turbo_view_context_generation)
|
|
146
|
+
class_registry.add(klass)
|
|
116
147
|
end
|
|
117
148
|
|
|
118
149
|
# A point-in-time snapshot of every registered streamable class, taken
|
|
@@ -121,13 +152,7 @@ module Phlex
|
|
|
121
152
|
# working; a class GC'd later simply won't appear next time. Call
|
|
122
153
|
# Rails.application.eager_load! FIRST if you need every app class loaded —
|
|
123
154
|
# the WeakMap only holds classes that have actually been loaded.
|
|
124
|
-
def registered_classes
|
|
125
|
-
@registry_mutex.synchronize do
|
|
126
|
-
classes = []
|
|
127
|
-
@registry.each_key { classes << it }
|
|
128
|
-
classes
|
|
129
|
-
end
|
|
130
|
-
end
|
|
155
|
+
def registered_classes = class_registry.to_a
|
|
131
156
|
|
|
132
157
|
# The ONE broadcast implementation shared by the class-level
|
|
133
158
|
# Streamable.broadcast_to and the module-level Phlex::Reactive.broadcast_to
|
|
@@ -423,14 +448,15 @@ module Phlex
|
|
|
423
448
|
# this class's generation — each thread rebuilds lazily on its next use,
|
|
424
449
|
# and entries for the old generation are dropped. Registered on Rails'
|
|
425
450
|
# reloader by the engine; also used by specs. Thread-safe (an integer
|
|
426
|
-
# bump; no shared mutable structure to tear down)
|
|
451
|
+
# bump; no shared mutable structure to tear down), and a class ivar
|
|
452
|
+
# write that only ever runs on the main Ractor: from the engine's
|
|
453
|
+
# to_prepare (boot, a development reload) or a spec.
|
|
427
454
|
def reset_turbo_view_context!
|
|
428
|
-
@turbo_view_context_generation = turbo_view_context_generation + 1
|
|
455
|
+
@turbo_view_context_generation = (turbo_view_context_generation || 0) + 1 # rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
429
456
|
end
|
|
430
457
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
end
|
|
458
|
+
# Set to 0 when the class registers (Streamable.register).
|
|
459
|
+
attr_reader :turbo_view_context_generation
|
|
434
460
|
|
|
435
461
|
# Render a component to HTML with a full Rails view context. Uses
|
|
436
462
|
# phlex-rails' #render_in against our memoized off-request view context
|
|
@@ -909,6 +935,10 @@ module Phlex
|
|
|
909
935
|
action: "remove", target: id, renders_root: false
|
|
910
936
|
)
|
|
911
937
|
end
|
|
938
|
+
|
|
939
|
+
# ActiveSupport::Concern's bookkeeping on this module, sealed now that
|
|
940
|
+
# the body has loaded (zoolutions/phlex-reactive#364).
|
|
941
|
+
Phlex::Reactive::Shareable.seal_concern(self)
|
|
912
942
|
end
|
|
913
943
|
end
|
|
914
944
|
end
|