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
data/lib/phlex/reactive.rb
CHANGED
|
@@ -115,44 +115,115 @@ module Phlex
|
|
|
115
115
|
# * request — the ActionDispatch::Request (remote_ip, headers, ...)
|
|
116
116
|
ActionContext = Data.define(:component, :action_name, :params, :request)
|
|
117
117
|
|
|
118
|
+
# What each setting reads before an app sets it, and after it is set to
|
|
119
|
+
# nil (zoolutions/phlex-reactive#364). Stored when the gem loads (the
|
|
120
|
+
# bottom of this module), so no reader writes on first use: a non-main
|
|
121
|
+
# Ractor can read a module instance variable but never write one.
|
|
122
|
+
SETTING_DEFAULTS = {
|
|
123
|
+
action_path: "/reactive/actions",
|
|
124
|
+
base_controller_name: "ActionController::Base",
|
|
125
|
+
early_event_ttl_ms: 10_000,
|
|
126
|
+
defer_token_ttl: 120,
|
|
127
|
+
defer_path: "/reactive/defer",
|
|
128
|
+
fragment_path: "/reactive/fragment",
|
|
129
|
+
fragment_cache_max_age_limit: 3600,
|
|
130
|
+
defer_transport: :auto,
|
|
131
|
+
defer_job_queue: "default",
|
|
132
|
+
settle_coalesce_window_ms: 50,
|
|
133
|
+
flash_target: "flash",
|
|
134
|
+
authorization_errors: [].freeze,
|
|
135
|
+
importmaps: nil,
|
|
136
|
+
apm: nil,
|
|
137
|
+
resolved_apm_adapter: nil,
|
|
138
|
+
effects: nil,
|
|
139
|
+
effects_generation: 0,
|
|
140
|
+
error_flash: nil,
|
|
141
|
+
flash_component: nil,
|
|
142
|
+
off_request_view_context_generation: 0,
|
|
143
|
+
around_actions: [].freeze,
|
|
144
|
+
on_action_error_hooks: [].freeze,
|
|
145
|
+
token_upgraders: {}.freeze,
|
|
146
|
+
param_types_frozen: false,
|
|
147
|
+
param_schemas_frozen: false,
|
|
148
|
+
finalized: false
|
|
149
|
+
}.freeze
|
|
150
|
+
|
|
151
|
+
# authorization_methods' default: the common set across authorization
|
|
152
|
+
# libraries (see authorization_methods below).
|
|
153
|
+
DEFAULT_AUTHORIZATION_METHODS = %i[authorize! authorize allowed_to?].freeze
|
|
154
|
+
|
|
155
|
+
# Every configuration reader whose value finalize! makes Ractor-shareable
|
|
156
|
+
# (the verifier aside: an ActiveSupport::MessageVerifier never is). The
|
|
157
|
+
# doctor's ractor readiness check and the finalize spec read this list.
|
|
158
|
+
CONFIG_READERS = %i[
|
|
159
|
+
renderer base_controller_name authorization_errors importmaps action_path verbose_errors
|
|
160
|
+
log_events apm debug verify_authorized authorization_methods param_types param_types_frozen?
|
|
161
|
+
param_schemas param_schemas_frozen? around_actions on_action_error_hooks resolved_apm_adapter
|
|
162
|
+
early_event_ttl_ms defer_token_ttl defer_path fragment_path fragment_cache_max_age_limit
|
|
163
|
+
defer_transport defer_job_queue settle_coalesce_window_ms flash_target effects effects_generation
|
|
164
|
+
error_flash flash_component off_request_view_context_generation token_upgraders finalized?
|
|
165
|
+
].freeze
|
|
166
|
+
|
|
118
167
|
class << self
|
|
119
168
|
# The message verifier used to sign/verify component identity tokens.
|
|
120
169
|
# Defaults to a purpose-scoped verifier derived from secret_key_base.
|
|
121
170
|
# Override to use a dedicated key.
|
|
122
|
-
|
|
171
|
+
def verifier=(value)
|
|
172
|
+
set_setting(:@verifier, value)
|
|
173
|
+
end
|
|
123
174
|
|
|
124
175
|
# The controller class used to render components with a full Rails view
|
|
125
176
|
# context (url helpers, CSRF, i18n) during re-renders and broadcasts.
|
|
126
177
|
# Defaults to ActionController::Base. Set to your ApplicationController if
|
|
127
178
|
# components rely on app-level helpers or Current attributes.
|
|
128
|
-
|
|
179
|
+
#
|
|
180
|
+
# Give it by name ("ApplicationController", like base_controller_name)
|
|
181
|
+
# to have it follow a code reload in development: the name resolves on
|
|
182
|
+
# every read, so a reloaded class is picked up (and the per-thread view
|
|
183
|
+
# contexts rebuild for it). A Class is kept as that class object, which
|
|
184
|
+
# a reload leaves stale.
|
|
185
|
+
def renderer=(value)
|
|
186
|
+
set_setting(:@renderer, value)
|
|
187
|
+
end
|
|
129
188
|
|
|
130
189
|
# The controller the reactive ActionsController inherits from, given as a
|
|
131
190
|
# String (resolved lazily to avoid load-order issues). Defaults to
|
|
132
191
|
# "ActionController::Base"; set to "ApplicationController" to inherit your
|
|
133
192
|
# app's auth/CSRF/Current. If you do, ensure the action path isn't
|
|
134
193
|
# force-redirected for logged-out users when you have public components.
|
|
135
|
-
|
|
194
|
+
def base_controller_name=(value)
|
|
195
|
+
set_setting(:@base_controller_name, value || SETTING_DEFAULTS[:base_controller_name])
|
|
196
|
+
end
|
|
136
197
|
|
|
137
198
|
# Exception classes the action endpoint renders as 403. Append your
|
|
138
199
|
# authorization library's error (Pundit::NotAuthorizedError,
|
|
139
|
-
# ActionPolicy::Unauthorized, ...)
|
|
140
|
-
|
|
200
|
+
# ActionPolicy::Unauthorized, ...) in an initializer, or assign the
|
|
201
|
+
# whole list. The list is frozen once boot is over (finalize!): after
|
|
202
|
+
# that, assign a new list instead of appending.
|
|
203
|
+
attr_reader :authorization_errors
|
|
204
|
+
|
|
205
|
+
def authorization_errors=(value)
|
|
206
|
+
set_setting(:@authorization_errors, value)
|
|
207
|
+
end
|
|
141
208
|
|
|
142
209
|
# Import maps `phlex_reactive:doctor` checks for the `phlex/reactive/early`
|
|
143
210
|
# pin (issue #307): a callable returning `{ name => Importmap::Map }`, for
|
|
144
211
|
# an app with more than one map (one per entry point). nil (the default)
|
|
145
212
|
# checks `Rails.application.importmap` only.
|
|
146
|
-
|
|
213
|
+
attr_reader :importmaps
|
|
214
|
+
|
|
215
|
+
def importmaps=(value)
|
|
216
|
+
set_setting(:@importmaps, value)
|
|
217
|
+
end
|
|
147
218
|
|
|
148
219
|
# The path the action endpoint is mounted at. Default "/reactive/actions".
|
|
149
220
|
# Set before boot if it collides with an app route. The client runtime
|
|
150
221
|
# reads it from a <meta name="phlex-reactive-action-path"> tag if present,
|
|
151
222
|
# falling back to this default.
|
|
152
|
-
|
|
223
|
+
attr_reader :action_path
|
|
153
224
|
|
|
154
|
-
def action_path
|
|
155
|
-
|
|
225
|
+
def action_path=(value)
|
|
226
|
+
set_setting(:@action_path, value || SETTING_DEFAULTS[:action_path])
|
|
156
227
|
end
|
|
157
228
|
|
|
158
229
|
# Diagnostic endpoint error bodies + dropped-param logging. When true, an
|
|
@@ -164,12 +235,12 @@ module Phlex
|
|
|
164
235
|
# Defaults LAZILY to Rails.env.local? — that's development AND test — so
|
|
165
236
|
# production stays opaque unless you opt in. The `defined?` guard (not
|
|
166
237
|
# `||=`) makes an explicit `= false` stick even in dev/test.
|
|
167
|
-
|
|
238
|
+
def verbose_errors=(value)
|
|
239
|
+
set_setting(:@verbose_errors, value)
|
|
240
|
+
end
|
|
168
241
|
|
|
169
242
|
def verbose_errors
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
defined?(::Rails.env) && ::Rails.env.local?
|
|
243
|
+
setting(:@verbose_errors) { defined?(::Rails.env) && ::Rails.env.local? }
|
|
173
244
|
end
|
|
174
245
|
|
|
175
246
|
# Attach the opt-in LogSubscriber (issue #107) so each hot-path event is
|
|
@@ -177,12 +248,12 @@ module Phlex
|
|
|
177
248
|
# (3.1ms)`). Default OFF — the events still fire for APMs regardless; this
|
|
178
249
|
# only controls the gem's own log lines. The engine reads it at boot and
|
|
179
250
|
# attaches exactly once. Set true in an initializer to see the lines.
|
|
180
|
-
|
|
251
|
+
def log_events=(value)
|
|
252
|
+
set_setting(:@log_events, value)
|
|
253
|
+
end
|
|
181
254
|
|
|
182
255
|
def log_events
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
false
|
|
256
|
+
setting(:@log_events) { false }
|
|
186
257
|
end
|
|
187
258
|
|
|
188
259
|
# Turnkey APM integration (issue #207). Set to a Symbol (:appsignal,
|
|
@@ -195,7 +266,11 @@ module Phlex
|
|
|
195
266
|
# action is named `Component#action` in the APM (not one blurry
|
|
196
267
|
# ActionsController#create), and an action-body error is reported to the
|
|
197
268
|
# tracker with component/action tags. Default nil.
|
|
198
|
-
|
|
269
|
+
attr_reader :apm
|
|
270
|
+
|
|
271
|
+
def apm=(value)
|
|
272
|
+
set_setting(:@apm, value)
|
|
273
|
+
end
|
|
199
274
|
|
|
200
275
|
# Client debug mode (issue #108): the "devtools-lite" lens on the reactive
|
|
201
276
|
# round trip. When true, reactive_attrs (and so reactive_root) stamps
|
|
@@ -207,12 +282,12 @@ module Phlex
|
|
|
207
282
|
# Rails.env.development?); zero cost when off (one nil-check per dispatch, no
|
|
208
283
|
# attr, no string building). The `defined?` guard (not `||=`) makes an
|
|
209
284
|
# explicit `= false` stick even where a truthy default might otherwise apply.
|
|
210
|
-
|
|
285
|
+
def debug=(value)
|
|
286
|
+
set_setting(:@debug, value)
|
|
287
|
+
end
|
|
211
288
|
|
|
212
289
|
def debug
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
false
|
|
290
|
+
setting(:@debug) { false }
|
|
216
291
|
end
|
|
217
292
|
|
|
218
293
|
# verify_authorized (issue #168): the default-ON runtime guard. When true,
|
|
@@ -223,12 +298,12 @@ module Phlex
|
|
|
223
298
|
# component with `skip_verify_authorized`, mark a bespoke check with
|
|
224
299
|
# `mark_authorized!`, or turn it off globally here. The `defined?` guard
|
|
225
300
|
# (not `||=`) makes an explicit `= false` stick.
|
|
226
|
-
|
|
301
|
+
def verify_authorized=(value)
|
|
302
|
+
set_setting(:@verify_authorized, value)
|
|
303
|
+
end
|
|
227
304
|
|
|
228
305
|
def verify_authorized
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
true
|
|
306
|
+
setting(:@verify_authorized) { true }
|
|
232
307
|
end
|
|
233
308
|
|
|
234
309
|
# The method names verify_authorized's interceptor wraps to detect an
|
|
@@ -238,12 +313,12 @@ module Phlex
|
|
|
238
313
|
# your app's helper. `mark_authorized!` always counts, regardless of this
|
|
239
314
|
# list. The `defined?` guard makes an explicit override (including a
|
|
240
315
|
# narrower list) stick.
|
|
241
|
-
|
|
316
|
+
def authorization_methods=(value)
|
|
317
|
+
set_setting(:@authorization_methods, value)
|
|
318
|
+
end
|
|
242
319
|
|
|
243
320
|
def authorization_methods
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
%i[authorize! authorize allowed_to?]
|
|
321
|
+
setting(:@authorization_methods) { DEFAULT_AUTHORIZATION_METHODS }
|
|
247
322
|
end
|
|
248
323
|
|
|
249
324
|
# Register an app-defined param type (issue #109). The block receives the
|
|
@@ -272,11 +347,11 @@ module Phlex
|
|
|
272
347
|
end
|
|
273
348
|
|
|
274
349
|
# The param-type registry: Symbol => callable(value) -> coerced | DROP.
|
|
275
|
-
# Seeded
|
|
276
|
-
# declared type symbol against it,
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
350
|
+
# Seeded with the built-ins when the gem loads (the bottom of this file);
|
|
351
|
+
# ParamSchema.compile validates every declared type symbol against it,
|
|
352
|
+
# and #coerce dispatches through it once per scalar param, so it is a
|
|
353
|
+
# plain attr_reader.
|
|
354
|
+
attr_reader :param_types
|
|
280
355
|
|
|
281
356
|
# A declared type symbol is known iff it's a registry key. ParamSchema
|
|
282
357
|
# asks this at compile so an unknown symbol raises loudly at declaration.
|
|
@@ -289,18 +364,16 @@ module Phlex
|
|
|
289
364
|
# Idempotent.
|
|
290
365
|
def freeze_param_types!
|
|
291
366
|
param_types.freeze
|
|
292
|
-
|
|
367
|
+
set_setting(:@param_types_frozen, true)
|
|
293
368
|
end
|
|
294
369
|
|
|
295
|
-
def param_types_frozen?
|
|
296
|
-
@param_types_frozen ||= false
|
|
297
|
-
end
|
|
370
|
+
def param_types_frozen? = setting(:@param_types_frozen) == true
|
|
298
371
|
|
|
299
372
|
# Drop the registry back to the built-ins and unfreeze it. For tests that
|
|
300
373
|
# register a temporary type; never called in production.
|
|
301
374
|
def reset_param_types!
|
|
302
|
-
|
|
303
|
-
|
|
375
|
+
set_setting(:@param_types, Phlex::Reactive::ParamSchema.built_in_types.dup, share: false)
|
|
376
|
+
set_setting(:@param_types_frozen, false)
|
|
304
377
|
end
|
|
305
378
|
|
|
306
379
|
# Register (with a schema Hash) OR read (bare name) a NAMED param schema
|
|
@@ -327,9 +400,8 @@ module Phlex
|
|
|
327
400
|
param_schemas[name.to_sym] = deep_freeze_schema(schema.transform_keys(&:to_sym))
|
|
328
401
|
end
|
|
329
402
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
end
|
|
403
|
+
# The registered named schemas: Symbol => deep-frozen schema Hash.
|
|
404
|
+
attr_reader :param_schemas
|
|
333
405
|
|
|
334
406
|
def param_schema?(name)
|
|
335
407
|
param_schemas.key?(name.to_sym)
|
|
@@ -337,17 +409,15 @@ module Phlex
|
|
|
337
409
|
|
|
338
410
|
def freeze_param_schemas!
|
|
339
411
|
param_schemas.freeze
|
|
340
|
-
|
|
412
|
+
set_setting(:@param_schemas_frozen, true)
|
|
341
413
|
end
|
|
342
414
|
|
|
343
|
-
def param_schemas_frozen?
|
|
344
|
-
@param_schemas_frozen ||= false
|
|
345
|
-
end
|
|
415
|
+
def param_schemas_frozen? = setting(:@param_schemas_frozen) == true
|
|
346
416
|
|
|
347
417
|
# Drop the named-schema registry and unfreeze it (tests only).
|
|
348
418
|
def reset_param_schemas!
|
|
349
|
-
|
|
350
|
-
|
|
419
|
+
set_setting(:@param_schemas, {}, share: false)
|
|
420
|
+
set_setting(:@param_schemas_frozen, false)
|
|
351
421
|
end
|
|
352
422
|
|
|
353
423
|
private
|
|
@@ -413,21 +483,20 @@ module Phlex
|
|
|
413
483
|
def around_action(&block)
|
|
414
484
|
raise ::ArgumentError, "Phlex::Reactive.around_action requires a block" unless block
|
|
415
485
|
|
|
416
|
-
around_actions
|
|
486
|
+
set_setting(:@around_actions, [*around_actions, block].freeze)
|
|
417
487
|
end
|
|
418
488
|
|
|
419
|
-
# The registered around_action wrappers, oldest first
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
end
|
|
489
|
+
# The registered around_action wrappers, oldest first (a frozen list,
|
|
490
|
+
# replaced whole on each registration). The endpoint folds them so the
|
|
491
|
+
# last-registered runs outermost; an empty stack is the default hot path
|
|
492
|
+
# (the controller short-circuits with `return yield`).
|
|
493
|
+
attr_reader :around_actions
|
|
425
494
|
|
|
426
495
|
# Drop all registered around_action wrappers. For test isolation (the
|
|
427
496
|
# shipped hook — specs registering a temporary wrapper reset around every
|
|
428
497
|
# example); never called in production.
|
|
429
498
|
def reset_around_actions!
|
|
430
|
-
|
|
499
|
+
set_setting(:@around_actions, SETTING_DEFAULTS[:around_actions])
|
|
431
500
|
end
|
|
432
501
|
|
|
433
502
|
# Register a block called when a reactive action body raises a
|
|
@@ -444,17 +513,15 @@ module Phlex
|
|
|
444
513
|
def on_action_error(&block)
|
|
445
514
|
raise ::ArgumentError, "Phlex::Reactive.on_action_error requires a block" unless block
|
|
446
515
|
|
|
447
|
-
on_action_error_hooks
|
|
516
|
+
set_setting(:@on_action_error_hooks, [*on_action_error_hooks, block].freeze)
|
|
448
517
|
end
|
|
449
518
|
|
|
450
|
-
# The registered on_action_error hooks, oldest first.
|
|
451
|
-
|
|
452
|
-
@on_action_error_hooks ||= []
|
|
453
|
-
end
|
|
519
|
+
# The registered on_action_error hooks, oldest first (a frozen list).
|
|
520
|
+
attr_reader :on_action_error_hooks
|
|
454
521
|
|
|
455
522
|
# Drop all registered on_action_error hooks. Test isolation; never in production.
|
|
456
523
|
def reset_on_action_error!
|
|
457
|
-
|
|
524
|
+
set_setting(:@on_action_error_hooks, SETTING_DEFAULTS[:on_action_error_hooks])
|
|
458
525
|
end
|
|
459
526
|
|
|
460
527
|
# The name-only keys forwarded to error reporters (issue #207). Deliberately
|
|
@@ -475,7 +542,7 @@ module Phlex
|
|
|
475
542
|
# side-effect only: the return value is ignored.
|
|
476
543
|
def report_error(error, context)
|
|
477
544
|
snapshot = context.slice(*ERROR_CONTEXT_KEYS)
|
|
478
|
-
adapter =
|
|
545
|
+
adapter = resolved_apm_adapter
|
|
479
546
|
# Each reporter runs through safely_report on its OWN — one broken reporter
|
|
480
547
|
# (a raising adapter or hook) never prevents the others, and never turns one
|
|
481
548
|
# 500 into a different 500. safely_report takes the reporter as a block arg,
|
|
@@ -494,14 +561,29 @@ module Phlex
|
|
|
494
561
|
# The APM adapter resolved at engine attach time (issue #207), or nil. Held so
|
|
495
562
|
# report_error can reach record_error without re-running detection per request.
|
|
496
563
|
# Set by APM.attach!; nil when no APM is configured or the SDK was absent.
|
|
497
|
-
|
|
564
|
+
attr_reader :resolved_apm_adapter
|
|
565
|
+
|
|
566
|
+
def resolved_apm_adapter=(value)
|
|
567
|
+
set_setting(:@resolved_apm_adapter, value)
|
|
568
|
+
end
|
|
498
569
|
|
|
570
|
+
# Built on first use (it needs Rails.application); finalize! builds it at
|
|
571
|
+
# boot. An ActiveSupport::MessageVerifier is never Ractor-shareable, and
|
|
572
|
+
# ActiveSupport cannot build or use one outside the main Ractor, so
|
|
573
|
+
# signing stays on the main Ractor (an ActiveSupport limit).
|
|
499
574
|
def verifier
|
|
500
|
-
|
|
575
|
+
setting(:@verifier) || set_setting(:@verifier, default_verifier)
|
|
501
576
|
end
|
|
502
577
|
|
|
578
|
+
# ActionController::Base unless an app set one; resolved on each read
|
|
579
|
+
# until finalize! stores it, so reading never writes. A name resolves on
|
|
580
|
+
# every read (see renderer=).
|
|
503
581
|
def renderer
|
|
504
|
-
|
|
582
|
+
case (value = setting(:@renderer))
|
|
583
|
+
when nil then defined?(::ActionController::Base) ? ::ActionController::Base : nil
|
|
584
|
+
when ::String then ::ActiveSupport::Inflector.constantize(value)
|
|
585
|
+
else value
|
|
586
|
+
end
|
|
505
587
|
end
|
|
506
588
|
|
|
507
589
|
# Build an off-request view context whose controller has a REAL `request`.
|
|
@@ -560,12 +642,11 @@ module Phlex
|
|
|
560
642
|
raise ArgumentError, "early_event_ttl_ms must be a positive Integer (milliseconds) or nil, got #{value.inspect}"
|
|
561
643
|
end
|
|
562
644
|
|
|
563
|
-
|
|
645
|
+
set_setting(:@early_event_ttl_ms, value || SETTING_DEFAULTS[:early_event_ttl_ms])
|
|
564
646
|
end
|
|
565
647
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
end
|
|
648
|
+
# The replay window, in milliseconds (default 10 s).
|
|
649
|
+
attr_reader :early_event_ttl_ms
|
|
569
650
|
|
|
570
651
|
# --- Deferred reply segments (issue #165) --------------------------
|
|
571
652
|
|
|
@@ -573,18 +654,18 @@ module Phlex
|
|
|
573
654
|
# reply→fetch gap (or reply→job→SSE on the push lane), so it stays short:
|
|
574
655
|
# a leaked defer token is a render oracle for exactly one component
|
|
575
656
|
# identity until this expires. nil resets to the default.
|
|
576
|
-
|
|
657
|
+
attr_reader :defer_token_ttl
|
|
577
658
|
|
|
578
|
-
def defer_token_ttl
|
|
579
|
-
|
|
659
|
+
def defer_token_ttl=(value)
|
|
660
|
+
set_setting(:@defer_token_ttl, value || SETTING_DEFAULTS[:defer_token_ttl])
|
|
580
661
|
end
|
|
581
662
|
|
|
582
663
|
# The path the defer endpoint is mounted at (the pull lane's target).
|
|
583
664
|
# Default "/reactive/defer"; set before boot if it collides.
|
|
584
|
-
|
|
665
|
+
attr_reader :defer_path
|
|
585
666
|
|
|
586
|
-
def defer_path
|
|
587
|
-
|
|
667
|
+
def defer_path=(value)
|
|
668
|
+
set_setting(:@defer_path, value || SETTING_DEFAULTS[:defer_path])
|
|
588
669
|
end
|
|
589
670
|
|
|
590
671
|
# The path prefix of the cacheable-fragment GET endpoint (issue #277):
|
|
@@ -592,10 +673,10 @@ module Phlex
|
|
|
592
673
|
# before boot if it collides. The shell renders the full URL, but the
|
|
593
674
|
# client only fetches one under the path it knows: when you change this,
|
|
594
675
|
# also render <meta name="phlex-reactive-fragment-path" content="…">.
|
|
595
|
-
|
|
676
|
+
attr_reader :fragment_path
|
|
596
677
|
|
|
597
|
-
def fragment_path
|
|
598
|
-
|
|
678
|
+
def fragment_path=(value)
|
|
679
|
+
set_setting(:@fragment_path, value || SETTING_DEFAULTS[:fragment_path])
|
|
599
680
|
end
|
|
600
681
|
|
|
601
682
|
# The longest max-age (seconds) a `reactive_lazy cache:` component may
|
|
@@ -612,12 +693,11 @@ module Phlex
|
|
|
612
693
|
"(or nil for the default) — got #{value.inspect}"
|
|
613
694
|
end
|
|
614
695
|
|
|
615
|
-
|
|
696
|
+
set_setting(:@fragment_cache_max_age_limit, seconds || SETTING_DEFAULTS[:fragment_cache_max_age_limit])
|
|
616
697
|
end
|
|
617
698
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
end
|
|
699
|
+
# The max-age cap, in seconds (default 3600).
|
|
700
|
+
attr_reader :fragment_cache_max_age_limit
|
|
621
701
|
|
|
622
702
|
# The deterministic, no-expiry, purpose-scoped fragment id of an identity
|
|
623
703
|
# payload, and its verification (issue #277). See Phlex::Reactive::Fragment.
|
|
@@ -630,9 +710,7 @@ module Phlex
|
|
|
630
710
|
# warning when the capability is absent). Validated at assignment — a
|
|
631
711
|
# typo'd transport must fail at the initializer, not silently at reply
|
|
632
712
|
# time. nil resets to the default.
|
|
633
|
-
|
|
634
|
-
@defer_transport ||= :auto
|
|
635
|
-
end
|
|
713
|
+
attr_reader :defer_transport
|
|
636
714
|
|
|
637
715
|
def defer_transport=(value)
|
|
638
716
|
value = value&.to_sym
|
|
@@ -642,17 +720,17 @@ module Phlex
|
|
|
642
720
|
"(got #{value.inspect})"
|
|
643
721
|
end
|
|
644
722
|
|
|
645
|
-
|
|
723
|
+
set_setting(:@defer_transport, value || SETTING_DEFAULTS[:defer_transport])
|
|
646
724
|
end
|
|
647
725
|
|
|
648
726
|
# The ActiveJob queue the push lane's DeferredRenderJob runs on. Default
|
|
649
727
|
# "default"; point it at a fast queue in production — a deferred segment
|
|
650
728
|
# is a UX-latency render, and letting it starve behind heavy jobs defeats
|
|
651
729
|
# the point. nil resets to the default.
|
|
652
|
-
|
|
730
|
+
attr_reader :defer_job_queue
|
|
653
731
|
|
|
654
|
-
def defer_job_queue
|
|
655
|
-
|
|
732
|
+
def defer_job_queue=(value)
|
|
733
|
+
set_setting(:@defer_job_queue, value || SETTING_DEFAULTS[:defer_job_queue])
|
|
656
734
|
end
|
|
657
735
|
|
|
658
736
|
# Signs a defer payload (issue #165): same shape + version stamp as the
|
|
@@ -802,10 +880,10 @@ module Phlex
|
|
|
802
880
|
# a handful of them instead of 177. The ROW streams are never coalesced.
|
|
803
881
|
# Needs pgbus with zoolutions/pgbus#465 on the peers path; without it the
|
|
804
882
|
# window is simply ignored. nil resets to the default.
|
|
805
|
-
|
|
883
|
+
attr_reader :settle_coalesce_window_ms
|
|
806
884
|
|
|
807
|
-
def settle_coalesce_window_ms
|
|
808
|
-
|
|
885
|
+
def settle_coalesce_window_ms=(value)
|
|
886
|
+
set_setting(:@settle_coalesce_window_ms, value || SETTING_DEFAULTS[:settle_coalesce_window_ms])
|
|
809
887
|
end
|
|
810
888
|
|
|
811
889
|
# Can reply.pending mint a settle handle at all? A settle has NO pull
|
|
@@ -822,11 +900,11 @@ module Phlex
|
|
|
822
900
|
|
|
823
901
|
# DOM id of the host-app container a Response#flash appends into.
|
|
824
902
|
# Default "flash"; override to match your layout's flash region.
|
|
825
|
-
|
|
826
|
-
@flash_target ||= "flash"
|
|
827
|
-
end
|
|
903
|
+
attr_reader :flash_target
|
|
828
904
|
|
|
829
|
-
|
|
905
|
+
def flash_target=(value)
|
|
906
|
+
set_setting(:@flash_target, value || SETTING_DEFAULTS[:flash_target])
|
|
907
|
+
end
|
|
830
908
|
|
|
831
909
|
# Global effects opt-in (issue #215). nil (the default) = OFF: no root
|
|
832
910
|
# attrs are emitted anywhere, the wire is byte-identical to pre-effects,
|
|
@@ -839,21 +917,17 @@ module Phlex
|
|
|
839
917
|
#
|
|
840
918
|
# Stored pre-normalized (wire strings), validated at WRITE time — a
|
|
841
919
|
# typo'd effect name raises here, not at render time.
|
|
842
|
-
|
|
843
|
-
defined?(@effects) ? @effects : nil
|
|
844
|
-
end
|
|
920
|
+
attr_reader :effects
|
|
845
921
|
|
|
846
922
|
def effects=(value)
|
|
847
|
-
|
|
848
|
-
|
|
923
|
+
set_setting(:@effects, Phlex::Reactive::Effects.normalize_config(value))
|
|
924
|
+
set_setting(:@effects_generation, effects_generation + 1)
|
|
849
925
|
end
|
|
850
926
|
|
|
851
927
|
# Monotonic write counter for effects= — the cache key half that lets
|
|
852
928
|
# each component class memoize its RESOLVED effect attrs (reactive_attrs
|
|
853
929
|
# is the token-signing hot path) without ever serving a stale config.
|
|
854
|
-
|
|
855
|
-
@effects_generation ||= 0
|
|
856
|
-
end
|
|
930
|
+
attr_reader :effects_generation
|
|
857
931
|
|
|
858
932
|
# A user-visible flash rendered on every endpoint rescue path (issue #100).
|
|
859
933
|
# Default nil = today's behavior (a bare head, or the verbose_errors
|
|
@@ -863,7 +937,11 @@ module Phlex
|
|
|
863
937
|
# — at the SAME status it returns today (statuses never change). Composes
|
|
864
938
|
# with verbose_errors: the turbo-stream flash wins the response body, the
|
|
865
939
|
# diagnostic still goes to the log.
|
|
866
|
-
|
|
940
|
+
attr_reader :error_flash
|
|
941
|
+
|
|
942
|
+
def error_flash=(value)
|
|
943
|
+
set_setting(:@error_flash, value)
|
|
944
|
+
end
|
|
867
945
|
|
|
868
946
|
# A CALLABLE that builds a flash component from STRING flash content (issue
|
|
869
947
|
# #182). Given (level, content), it returns a Phlex component the gem renders
|
|
@@ -886,7 +964,7 @@ module Phlex
|
|
|
886
964
|
"flash_component = ->(level, content) { #{callable}.new(level:, content:) }"
|
|
887
965
|
end
|
|
888
966
|
|
|
889
|
-
|
|
967
|
+
set_setting(:@flash_component, callable)
|
|
890
968
|
end
|
|
891
969
|
|
|
892
970
|
# Render a Phlex component to HTML with a full (off-request) view context.
|
|
@@ -960,7 +1038,7 @@ module Phlex
|
|
|
960
1038
|
# reset_flash_builder! (issue #113); the old name stays a permanent alias
|
|
961
1039
|
# below so the engine's to_prepare hook keeps working.
|
|
962
1040
|
def reset_stream_builder!
|
|
963
|
-
|
|
1041
|
+
set_setting(:@off_request_view_context_generation, off_request_view_context_generation + 1)
|
|
964
1042
|
end
|
|
965
1043
|
|
|
966
1044
|
# Issue #182: the pre-#113 aliases are removed (they contradicted the
|
|
@@ -976,9 +1054,8 @@ module Phlex
|
|
|
976
1054
|
"Phlex::Reactive.reset_flash_builder! was removed in issue #182 — use reset_stream_builder!"
|
|
977
1055
|
end
|
|
978
1056
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
end
|
|
1057
|
+
# The counter reset_stream_builder! bumps; the per-thread caches compare it.
|
|
1058
|
+
attr_reader :off_request_view_context_generation
|
|
982
1059
|
|
|
983
1060
|
private
|
|
984
1061
|
|
|
@@ -1002,9 +1079,8 @@ module Phlex
|
|
|
1002
1079
|
|
|
1003
1080
|
public
|
|
1004
1081
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
end
|
|
1082
|
+
# The base controller's name (see base_controller_name= above).
|
|
1083
|
+
attr_reader :base_controller_name
|
|
1008
1084
|
|
|
1009
1085
|
def base_controller
|
|
1010
1086
|
base_controller_name.constantize
|
|
@@ -1061,18 +1137,18 @@ module Phlex
|
|
|
1061
1137
|
def register_token_upgrader(from_version, &block)
|
|
1062
1138
|
raise ::ArgumentError, "register_token_upgrader requires a block" unless block
|
|
1063
1139
|
|
|
1064
|
-
token_upgraders
|
|
1140
|
+
set_setting(:@token_upgraders, token_upgraders.merge(Integer(from_version) => block).freeze)
|
|
1141
|
+
block
|
|
1065
1142
|
end
|
|
1066
1143
|
|
|
1067
1144
|
# from_version => callable(payload) -> migrated payload. Sparse: an entry
|
|
1068
|
-
# exists only for a version that actually changed shape.
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
end
|
|
1145
|
+
# exists only for a version that actually changed shape. A frozen Hash,
|
|
1146
|
+
# replaced whole on each registration.
|
|
1147
|
+
attr_reader :token_upgraders
|
|
1072
1148
|
|
|
1073
1149
|
# Drop all registered upgraders. For tests that register a temporary one.
|
|
1074
1150
|
def reset_token_upgraders!
|
|
1075
|
-
|
|
1151
|
+
set_setting(:@token_upgraders, SETTING_DEFAULTS[:token_upgraders])
|
|
1076
1152
|
end
|
|
1077
1153
|
|
|
1078
1154
|
# The acting client's SSE connection id during an action, or nil. Set by
|
|
@@ -1126,6 +1202,47 @@ module Phlex
|
|
|
1126
1202
|
{ protocol: request.protocol, host: request.host, port: request.optional_port }
|
|
1127
1203
|
end
|
|
1128
1204
|
|
|
1205
|
+
# --- Boot finalization (zoolutions/phlex-reactive#364) --------------
|
|
1206
|
+
|
|
1207
|
+
# Finalize the configuration once boot is over. The engine calls it
|
|
1208
|
+
# after every initializer AND every after_initialize block of the app has
|
|
1209
|
+
# run; an app booting phlex-reactive without the engine calls it at the
|
|
1210
|
+
# end of its own boot.
|
|
1211
|
+
#
|
|
1212
|
+
# It builds the one lazy default (the verifier) and resolves the rest,
|
|
1213
|
+
# freezes the param-type and named-schema registries, and stores every
|
|
1214
|
+
# setting as a Ractor-shareable value (Shareable.share: deep-frozen
|
|
1215
|
+
# COPIES, so the objects an app handed in are never frozen behind its
|
|
1216
|
+
# back), so a non-main Ractor can read the configuration. What cannot
|
|
1217
|
+
# be shared stays as it is and works on the main Ractor only: the
|
|
1218
|
+
# verifier (an ActiveSupport::MessageVerifier), and a Proc whose self is
|
|
1219
|
+
# not shareable (a block written in an initializer) — the
|
|
1220
|
+
# phlex_reactive:doctor ractor readiness check lists them.
|
|
1221
|
+
#
|
|
1222
|
+
# Writers keep working afterwards (a test suite sets config per
|
|
1223
|
+
# example) and what they write is shared too, but the lists are frozen:
|
|
1224
|
+
# assign a new authorization_errors list rather than appending to it.
|
|
1225
|
+
# Idempotent.
|
|
1226
|
+
def finalize!
|
|
1227
|
+
build_lazy_defaults
|
|
1228
|
+
freeze_param_types!
|
|
1229
|
+
freeze_param_schemas!
|
|
1230
|
+
set_setting(:@finalized, true)
|
|
1231
|
+
instance_variables.each { set_setting(it, Shareable.share(setting(it))) }
|
|
1232
|
+
self
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
def finalized? = setting(:@finalized) == true
|
|
1236
|
+
|
|
1237
|
+
# Back to the configuration before finalize! (tests only): finalized?
|
|
1238
|
+
# is false and authorization_errors can be appended to again. The
|
|
1239
|
+
# param-type and schema registries have their own resets.
|
|
1240
|
+
def reset_finalization!
|
|
1241
|
+
set_setting(:@finalized, false)
|
|
1242
|
+
set_setting(:@authorization_errors, authorization_errors.dup, share: false)
|
|
1243
|
+
self
|
|
1244
|
+
end
|
|
1245
|
+
|
|
1129
1246
|
# The controller a correctly-mounted action path resolves to. Used by the
|
|
1130
1247
|
# route guard below.
|
|
1131
1248
|
ACTIONS_CONTROLLER = "phlex/reactive/actions"
|
|
@@ -1181,6 +1298,49 @@ module Phlex
|
|
|
1181
1298
|
|
|
1182
1299
|
private
|
|
1183
1300
|
|
|
1301
|
+
# Every lazy reader, built and stored, so that after boot each one is a
|
|
1302
|
+
# plain read of a stored value.
|
|
1303
|
+
def build_lazy_defaults
|
|
1304
|
+
build_verifier
|
|
1305
|
+
set_setting(:@renderer, renderer) if setting(:@renderer).nil?
|
|
1306
|
+
set_setting(:@verbose_errors, verbose_errors)
|
|
1307
|
+
set_setting(:@log_events, log_events)
|
|
1308
|
+
set_setting(:@debug, debug)
|
|
1309
|
+
set_setting(:@verify_authorized, verify_authorized)
|
|
1310
|
+
set_setting(:@authorization_methods, authorization_methods)
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
# The verifier needs either one set explicitly or a Rails application;
|
|
1314
|
+
# without both it stays lazy (and raises its guided error on first use,
|
|
1315
|
+
# as before). So does an app that boots without a secret_key_base (a
|
|
1316
|
+
# production rake task or build step): Rails raises ArgumentError for
|
|
1317
|
+
# it, and boot must not, so the first token raises it instead.
|
|
1318
|
+
def build_verifier
|
|
1319
|
+
return unless setting(:@verifier) || (defined?(::Rails) && ::Rails.respond_to?(:application) && ::Rails.application)
|
|
1320
|
+
|
|
1321
|
+
verifier
|
|
1322
|
+
rescue ::ArgumentError
|
|
1323
|
+
nil
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
# --- Settings storage (zoolutions/phlex-reactive#364) ---------------
|
|
1327
|
+
# Every setting is an instance variable of this module, read and written
|
|
1328
|
+
# only through these two methods. Safe: settings are written at boot, by
|
|
1329
|
+
# the app's initializers on the main Ractor before it serves a request
|
|
1330
|
+
# (and between examples in a test suite); a non-main Ractor can never
|
|
1331
|
+
# write a module instance variable at all. After finalize! every value
|
|
1332
|
+
# is Ractor-shareable (or frozen, when it holds an app's Proc) and every
|
|
1333
|
+
# read is a plain read; a write after it is shared the same way, except
|
|
1334
|
+
# the test-only resets of the registries (share: false), which reopen
|
|
1335
|
+
# them.
|
|
1336
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
1337
|
+
def setting(ivar) = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : (yield if block_given?)
|
|
1338
|
+
|
|
1339
|
+
def set_setting(ivar, value, share: true)
|
|
1340
|
+
instance_variable_set(ivar, share && setting(:@finalized) == true ? Shareable.share(value) : value)
|
|
1341
|
+
end
|
|
1342
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
1343
|
+
|
|
1184
1344
|
# Materialize the route set if it hasn't been drawn yet (the engine appends
|
|
1185
1345
|
# POST /reactive/actions when routes are drawn, which may be after the boot
|
|
1186
1346
|
# guard's after_initialize). Idempotent; tolerant of older Rails.
|
|
@@ -1238,7 +1398,14 @@ module Phlex
|
|
|
1238
1398
|
end
|
|
1239
1399
|
end
|
|
1240
1400
|
|
|
1401
|
+
# Every setting starts at its default as the gem loads, so no reader
|
|
1402
|
+
# writes on first use (the one lazy setting is the verifier, which needs
|
|
1403
|
+
# Rails.application; finalize! builds it at boot; the param-type registry
|
|
1404
|
+
# is seeded below the loader). The authorization_errors list stays open
|
|
1405
|
+
# for appending until finalize!.
|
|
1406
|
+
SETTING_DEFAULTS.each { |name, value| instance_variable_set(:"@#{name}", value) }
|
|
1241
1407
|
self.authorization_errors = []
|
|
1408
|
+
reset_param_schemas!
|
|
1242
1409
|
end
|
|
1243
1410
|
end
|
|
1244
1411
|
|
|
@@ -1295,4 +1462,8 @@ loader.do_not_eager_load("#{__dir__}/reactive/deferred_render_job.rb")
|
|
|
1295
1462
|
loader.do_not_eager_load("#{__dir__}/reactive/presence_render_job.rb")
|
|
1296
1463
|
loader.setup
|
|
1297
1464
|
|
|
1465
|
+
# The param-type registry's built-ins (ParamSchema needs the loader, and loads
|
|
1466
|
+
# ActiveModel, which the engine's boot needed already).
|
|
1467
|
+
Phlex::Reactive.reset_param_types!
|
|
1468
|
+
|
|
1298
1469
|
require "phlex/reactive/engine" if defined?(Rails::Engine)
|