upkeep-rails 0.2.5-aarch64-linux-gnu

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.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +244 -0
  4. data/docs/drafts/turbo-streams-is-cache-invalidation-you-write-by-hand.md +240 -0
  5. data/docs/how-it-works.md +329 -0
  6. data/docs/plans/sqlglot-active-record-migration.md +211 -0
  7. data/docs/plans/turbo-frame-subscription-composition.md +90 -0
  8. data/docs/spikes/SQLGLOT_VS_AREL_FINDINGS.md +136 -0
  9. data/docs/spikes/query-source-analysis-comparison/FINDINGS.md +169 -0
  10. data/docs/spikes/sqlglot-first-active-record/README.md +26 -0
  11. data/docs/spikes/sqlglot-query-analysis/FINDINGS.md +110 -0
  12. data/docs/spikes/sqlglot-query-analysis/PULSE_1802_FINDINGS.md +95 -0
  13. data/docs/spikes/sqlglot-semantic-bindings/README.md +29 -0
  14. data/lib/generators/upkeep/install/install_generator.rb +192 -0
  15. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
  16. data/lib/generators/upkeep/install/templates/subscription.js +288 -0
  17. data/lib/generators/upkeep/install/templates/upkeep.rb +65 -0
  18. data/lib/upkeep/active_record_query.rb +248 -0
  19. data/lib/upkeep/capture/request.rb +150 -0
  20. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  21. data/lib/upkeep/dag.rb +454 -0
  22. data/lib/upkeep/delivery/action_cable_adapter.rb +48 -0
  23. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  24. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  25. data/lib/upkeep/delivery/transport.rb +194 -0
  26. data/lib/upkeep/delivery/turbo_streams.rb +339 -0
  27. data/lib/upkeep/delivery.rb +7 -0
  28. data/lib/upkeep/dependencies.rb +600 -0
  29. data/lib/upkeep/herb/developer_report.rb +135 -0
  30. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  31. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  32. data/lib/upkeep/herb/source_instrumenter.rb +149 -0
  33. data/lib/upkeep/herb/template_manifest.rb +548 -0
  34. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  35. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  36. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  37. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  38. data/lib/upkeep/invalidation/planner.rb +411 -0
  39. data/lib/upkeep/invalidation.rb +7 -0
  40. data/lib/upkeep/rails/action_view_capture.rb +1007 -0
  41. data/lib/upkeep/rails/activation_token.rb +55 -0
  42. data/lib/upkeep/rails/cable/channel.rb +165 -0
  43. data/lib/upkeep/rails/cable/subscriber_identity.rb +361 -0
  44. data/lib/upkeep/rails/cable.rb +4 -0
  45. data/lib/upkeep/rails/client_subscription.rb +65 -0
  46. data/lib/upkeep/rails/cluster_guard.rb +57 -0
  47. data/lib/upkeep/rails/configuration.rb +252 -0
  48. data/lib/upkeep/rails/controller_runtime.rb +187 -0
  49. data/lib/upkeep/rails/install.rb +28 -0
  50. data/lib/upkeep/rails/job_runtime.rb +43 -0
  51. data/lib/upkeep/rails/railtie.rb +44 -0
  52. data/lib/upkeep/rails/replay.rb +244 -0
  53. data/lib/upkeep/rails/testing.rb +259 -0
  54. data/lib/upkeep/rails.rb +466 -0
  55. data/lib/upkeep/replay.rb +462 -0
  56. data/lib/upkeep/runtime.rb +1276 -0
  57. data/lib/upkeep/shared_streams.rb +86 -0
  58. data/lib/upkeep/sql_dependency_analysis.rb +553 -0
  59. data/lib/upkeep/sqlglot/libsqlglot_rust.so +0 -0
  60. data/lib/upkeep/sqlglot/native.rb +121 -0
  61. data/lib/upkeep/sqlglot/native_library.rb +23 -0
  62. data/lib/upkeep/sqlglot.rb +367 -0
  63. data/lib/upkeep/subscriptions/active_record_store.rb +398 -0
  64. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +411 -0
  65. data/lib/upkeep/subscriptions/active_registry.rb +80 -0
  66. data/lib/upkeep/subscriptions/base_store.rb +110 -0
  67. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  68. data/lib/upkeep/subscriptions/layered_reverse_index.rb +125 -0
  69. data/lib/upkeep/subscriptions/lookup_instrumentation.rb +32 -0
  70. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +228 -0
  71. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  72. data/lib/upkeep/subscriptions/reverse_index.rb +313 -0
  73. data/lib/upkeep/subscriptions/shape.rb +117 -0
  74. data/lib/upkeep/subscriptions/store.rb +349 -0
  75. data/lib/upkeep/subscriptions.rb +7 -0
  76. data/lib/upkeep/targeting.rb +146 -0
  77. data/lib/upkeep/version.rb +5 -0
  78. data/lib/upkeep-rails.rb +3 -0
  79. data/lib/upkeep.rb +15 -0
  80. data/upkeep-rails.gemspec +66 -0
  81. metadata +327 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ # Detects boot configurations where live updates silently break across
6
+ # cluster workers: an in-process cable adapter or an in-memory
7
+ # subscription store cannot reach browsers or subscriptions held by
8
+ # another process.
9
+ class ClusterGuard
10
+ IN_PROCESS_CABLE_ADAPTERS = %w[async].freeze
11
+
12
+ attr_reader :cable_adapter, :worker_count, :subscription_store, :environment
13
+
14
+ def initialize(cable_adapter:, worker_count:, subscription_store:, environment:)
15
+ @cable_adapter = cable_adapter.to_s
16
+ @worker_count = worker_count.to_i
17
+ @subscription_store = subscription_store&.to_sym
18
+ @environment = environment.to_s
19
+ end
20
+
21
+ def clustered?
22
+ worker_count.positive?
23
+ end
24
+
25
+ def problems
26
+ return [] unless clustered?
27
+
28
+ problems = []
29
+ if IN_PROCESS_CABLE_ADAPTERS.include?(cable_adapter)
30
+ problems << "the #{cable_adapter} Action Cable adapter is in-process, so broadcasts from one worker " \
31
+ "never reach sockets held by another; configure a cross-process cable adapter such as solid_cable " \
32
+ "or redis in config/cable.yml"
33
+ end
34
+ if subscription_store == :memory
35
+ problems << "subscription_store=:memory is per-process, so subscriptions registered in one worker are " \
36
+ "invisible to the others; set config.upkeep.subscription_store = :active_record"
37
+ end
38
+ problems
39
+ end
40
+
41
+ def error?
42
+ problems.any? && environment == "production"
43
+ end
44
+
45
+ def warning?
46
+ problems.any? && !error?
47
+ end
48
+
49
+ def message
50
+ return if problems.empty?
51
+
52
+ "Upkeep detected a clustered server (#{worker_count} workers) with a configuration that cannot " \
53
+ "deliver live updates across processes: #{problems.join("; ")}."
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,252 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ class ConfigurationError < StandardError; end
6
+
7
+ class Configuration
8
+ SUBSCRIPTION_STORES = [:active_record, :memory].freeze
9
+ REFUSED_BOUNDARY_BEHAVIORS = [:raise, :warn].freeze
10
+ REQUEST_ACTIVATIONS = [:all, :opt_in].freeze
11
+ IDENTITY_SOURCES = [:current, :session, :cookie, :warden].freeze
12
+
13
+ class IdentityDefinition
14
+ attr_reader :name, :source, :source_key, :subscribe_block
15
+
16
+ def initialize(name:, source:, source_key:, subscribe_block:, absent_block: nil)
17
+ @name = name.to_sym
18
+ @source = source.to_sym
19
+ @source_key = source_key
20
+ @subscribe_block = subscribe_block
21
+ @absent_block = absent_block
22
+ end
23
+
24
+ def matches_dependency?(dependency)
25
+ case source
26
+ when :current
27
+ dependency.source == :current_attribute &&
28
+ metadata_value(dependency, :owner) == source_key.fetch(:owner) &&
29
+ metadata_value(dependency, :name) == source_key.fetch(:name)
30
+ when :session
31
+ dependency.source == :session && metadata_value(dependency, :key) == source_key
32
+ when :cookie
33
+ dependency.source == :cookie && metadata_value(dependency, :key) == source_key
34
+ when :warden
35
+ dependency.source == :warden_user && metadata_value(dependency, :scope) == source_key
36
+ else
37
+ false
38
+ end
39
+ end
40
+
41
+ def matches_source?(source, key)
42
+ source = source.to_sym
43
+
44
+ case self.source
45
+ when :current
46
+ source == :current &&
47
+ key.fetch(:owner).to_s == source_key.fetch(:owner) &&
48
+ key.fetch(:name).to_s == source_key.fetch(:name)
49
+ when :session, :cookie, :warden
50
+ source == self.source && key.to_s == source_key
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ def absent?(value)
57
+ return value.nil? unless @absent_block
58
+
59
+ @absent_block.arity == 1 ? @absent_block.call(value) : @absent_block.call
60
+ end
61
+
62
+ def absent_dependency?(dependency)
63
+ Upkeep::Dependencies.identity_absent_for?(dependency, name)
64
+ end
65
+
66
+ def source_label
67
+ case source
68
+ when :current
69
+ "#{source_key.fetch(:owner)}.#{source_key.fetch(:name)}"
70
+ when :session
71
+ "session[:#{source_key}]"
72
+ when :cookie
73
+ "cookies[:#{source_key}]"
74
+ when :warden
75
+ "warden.user(:#{source_key})"
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def metadata_value(dependency, key)
82
+ value = dependency.metadata[key] || dependency.metadata[key.to_s]
83
+ value.to_s
84
+ end
85
+ end
86
+
87
+ class IdentityBuilder
88
+ attr_reader :subscribe_block, :absent_block
89
+
90
+ def absent_if(&block)
91
+ @absent_block = block
92
+ end
93
+
94
+ def subscribe(&block)
95
+ @subscribe_block = block
96
+ end
97
+ end
98
+
99
+ attr_accessor :enabled
100
+ attr_accessor :activation_token_expires_in
101
+ attr_accessor :delivery_batch_window
102
+ # Seconds a subscription may go untouched before it counts as abandoned
103
+ # and becomes eligible for pruning. Connected pages are kept alive by the
104
+ # cable channel heartbeat, which fires far more often than this TTL.
105
+ attr_accessor :subscription_ttl
106
+ # Test/console hook: deliver changes synchronously in the caller instead
107
+ # of on the in-process background dispatcher.
108
+ attr_accessor :deliver_inline
109
+ attr_reader :subscription_store
110
+ attr_reader :request_activation
111
+
112
+ def initialize
113
+ @enabled = true
114
+ @subscription_store = :active_record
115
+ @deliver_inline = false
116
+ @delivery_batch_window = 0.01
117
+ @refused_boundary_behavior = nil
118
+ @request_activation = :all
119
+ @activation_token_expires_in = 24 * 60 * 60
120
+ @subscription_ttl = 24 * 60 * 60
121
+ @identity_definitions = {}
122
+ end
123
+
124
+ def subscription_store=(value)
125
+ value = value.to_sym if value.respond_to?(:to_sym)
126
+
127
+ unless SUBSCRIPTION_STORES.include?(value)
128
+ raise ConfigurationError,
129
+ "Unknown Upkeep subscription_store #{value.inspect}; expected one of #{SUBSCRIPTION_STORES.join(", ")}"
130
+ end
131
+
132
+ @subscription_store = value
133
+ end
134
+
135
+ def refused_boundary_behavior
136
+ @refused_boundary_behavior || default_refused_boundary_behavior
137
+ end
138
+
139
+ def refused_boundary_behavior=(value)
140
+ value = value.to_sym if value.respond_to?(:to_sym)
141
+
142
+ unless REFUSED_BOUNDARY_BEHAVIORS.include?(value)
143
+ raise ConfigurationError,
144
+ "Unknown Upkeep refused_boundary_behavior #{value.inspect}; expected one of #{REFUSED_BOUNDARY_BEHAVIORS.join(", ")}"
145
+ end
146
+
147
+ @refused_boundary_behavior = value
148
+ end
149
+
150
+ def request_activation=(value)
151
+ value = value.to_sym if value.respond_to?(:to_sym)
152
+
153
+ unless REQUEST_ACTIVATIONS.include?(value)
154
+ raise ConfigurationError,
155
+ "Unknown Upkeep request_activation #{value.inspect}; expected one of #{REQUEST_ACTIVATIONS.join(", ")}"
156
+ end
157
+
158
+ @request_activation = value
159
+ end
160
+
161
+ def identify(name, current: nil, session: nil, cookie: nil, warden: nil, &block)
162
+ source, source_key = identity_source(current: current, session: session, cookie: cookie, warden: warden)
163
+ builder = IdentityBuilder.new
164
+ if block
165
+ block.arity == 1 ? block.call(builder) : builder.instance_eval(&block)
166
+ end
167
+
168
+ unless builder.subscribe_block
169
+ raise ConfigurationError, "config.identify :#{name} requires a subscribe block"
170
+ end
171
+
172
+ @identity_definitions[name.to_sym] = IdentityDefinition.new(
173
+ name: name,
174
+ source: source,
175
+ source_key: source_key,
176
+ subscribe_block: builder.subscribe_block,
177
+ absent_block: builder.absent_block
178
+ )
179
+ end
180
+
181
+ def identity_presence_metadata(source:, key:, value:)
182
+ definitions = identity_definitions.select { |definition| definition.matches_source?(source, key) }
183
+ absent_by_name = definitions.to_h do |definition|
184
+ [definition.name.to_s, definition.absent?(value)]
185
+ end
186
+
187
+ {
188
+ partitioning: if definitions.any?
189
+ absent_by_name.values.any? { |absent| !absent }
190
+ else
191
+ !value.nil?
192
+ end,
193
+ absent_by_name: absent_by_name
194
+ }
195
+ end
196
+
197
+ def identity_definitions
198
+ @identity_definitions.values
199
+ end
200
+
201
+ def identity_definition(name)
202
+ @identity_definitions.fetch(name.to_sym)
203
+ end
204
+
205
+ def clear_identities!
206
+ @identity_definitions.clear
207
+ end
208
+
209
+ private
210
+
211
+ def identity_source(current:, session:, cookie:, warden:)
212
+ sources = {
213
+ current: current,
214
+ session: session,
215
+ cookie: cookie,
216
+ warden: warden
217
+ }.compact
218
+
219
+ unless sources.size == 1
220
+ raise ConfigurationError,
221
+ "config.identify requires exactly one source: #{IDENTITY_SOURCES.join(", ")}"
222
+ end
223
+
224
+ source, value = sources.first
225
+ [source, normalize_identity_source(source, value)]
226
+ end
227
+
228
+ def normalize_identity_source(source, value)
229
+ case source
230
+ when :current
231
+ owner, name = Array(value)
232
+ unless owner && name
233
+ raise ConfigurationError,
234
+ "config.identify current: expects [CurrentClass, :attribute]"
235
+ end
236
+
237
+ { owner: owner.to_s, name: name.to_s }
238
+ when :session, :cookie, :warden
239
+ value.to_s
240
+ end
241
+ end
242
+
243
+ def default_refused_boundary_behavior
244
+ if defined?(::Rails) && ::Rails.respond_to?(:env) && ::Rails.env.to_s == "production"
245
+ :warn
246
+ else
247
+ :raise
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Upkeep
6
+ module Rails
7
+ module ControllerRuntime
8
+ extend ActiveSupport::Concern
9
+
10
+ SUPPRESS_KEY = :upkeep_rails_controller_runtime_suppressed
11
+
12
+ included do
13
+ class_attribute :upkeep_reactive_actions, instance_accessor: false, default: [].freeze
14
+ prepend_around_action :upkeep_capture_request
15
+ end
16
+
17
+ class_methods do
18
+ def upkeep_reactive(only:)
19
+ self.upkeep_reactive_actions = Array(only).map(&:to_s).freeze
20
+ end
21
+ end
22
+
23
+ # Public override point for host controllers. Return false to skip subscription
24
+ # capture and registration for this request. Writes are still captured and can
25
+ # update subscribed pages.
26
+ def upkeep_reactive_request?
27
+ return true if Upkeep::Rails.configuration.request_activation == :all
28
+
29
+ self.class.upkeep_reactive_actions.include?(action_name) || upkeep_reactive_continuation_request?
30
+ end
31
+
32
+ module_function
33
+
34
+ def install
35
+ return if @installed
36
+ return unless defined?(::ActionController::Base)
37
+
38
+ ::ActionController::Base.include(self)
39
+ @installed = true
40
+ end
41
+
42
+ def installed?
43
+ !!@installed
44
+ end
45
+
46
+ def reset!
47
+ @installed = false
48
+ end
49
+
50
+ def suppress
51
+ previous = Thread.current[SUPPRESS_KEY]
52
+ Thread.current[SUPPRESS_KEY] = true
53
+ yield
54
+ ensure
55
+ Thread.current[SUPPRESS_KEY] = previous
56
+ end
57
+
58
+ def suppressed?
59
+ Thread.current[SUPPRESS_KEY]
60
+ end
61
+
62
+ private
63
+
64
+ def upkeep_capture_request(&action)
65
+ return action.call if ControllerRuntime.suppressed?
66
+ return action.call if Upkeep::Runtime::Observation.recorder
67
+
68
+ payload = {
69
+ controller: self.class.name,
70
+ action: action_name,
71
+ method: request.request_method,
72
+ path: request.fullpath,
73
+ subscription_request: upkeep_subscription_request?
74
+ }
75
+ ActiveSupport::Notifications.instrument(Upkeep::Rails::REQUEST_CAPTURE, payload) do
76
+ upkeep_capture_request_with_timing(action, payload)
77
+ end
78
+ end
79
+
80
+ def upkeep_capture_request_with_timing(action, payload)
81
+ measure_phase(payload, :deliver_pending_ms) { Upkeep::Rails.deliver_changes_now! }
82
+
83
+ result = nil
84
+ capture = nil
85
+ changes = []
86
+ measure_phase(payload, :change_capture_ms) do
87
+ _captured, changes = Upkeep::Runtime::ChangeLog.capture do
88
+ if payload.fetch(:subscription_request)
89
+ capture = Upkeep::Capture::Request.call(self, profile: request_capture_profile?) { action.call }
90
+ result = capture.action_result
91
+ else
92
+ measure_phase(payload, :action_ms) { result = action.call }
93
+ end
94
+ end
95
+ end
96
+ changes = upkeep_stamp_change_request_id(changes)
97
+ record_capture_payload(payload, capture) if capture
98
+
99
+ measure_phase(payload, :deliver_changes_ms) do
100
+ if capture
101
+ Upkeep::Rails.deliver_changes_now!(changes)
102
+ else
103
+ Upkeep::Rails.deliver_changes!(changes)
104
+ end
105
+ end
106
+
107
+ registration = nil
108
+ if capture
109
+ measure_phase(payload, :register_ms) do
110
+ registration = Upkeep::Rails.register_controller_subscription(self, capture)
111
+ end
112
+ end
113
+ payload[:registered] = !!registration
114
+ if capture && registration
115
+ measure_phase(payload, :inject_ms) do
116
+ Upkeep::Rails::ClientSubscription.headers_for(
117
+ identity: registration.identity,
118
+ subscription: registration.subscription
119
+ ).each { |name, value| response.headers[name] = value }
120
+ response.body = Upkeep::Rails::ClientSubscription.inject(
121
+ capture.html,
122
+ identity: registration.identity,
123
+ subscription: registration.subscription
124
+ )
125
+ end
126
+ payload[:subscription_id] = registration.subscription.id
127
+ end
128
+
129
+ result
130
+ end
131
+
132
+ # GET writes can otherwise self-refresh in a loop. Mutation deliveries must
133
+ # still reach the originating tab when the response omits an affected region.
134
+ def upkeep_stamp_change_request_id(changes)
135
+ return changes unless request.get? || request.head?
136
+
137
+ request_id = upkeep_turbo_request_id
138
+ return changes unless request_id
139
+
140
+ changes.map { |change| change.respond_to?(:merge) ? change.merge(request_id: request_id) : change }
141
+ end
142
+
143
+ def upkeep_turbo_request_id
144
+ turbo_request_id = ::Turbo.current_request_id if defined?(::Turbo) && ::Turbo.respond_to?(:current_request_id)
145
+ turbo_request_id || request.headers["X-Turbo-Request-Id"]
146
+ end
147
+
148
+ def record_capture_payload(payload, capture)
149
+ payload[:response_status] = capture.response_status
150
+ payload[:response_content_type] = capture.response_content_type
151
+ payload[:response_media_type] = capture.response_media_type
152
+ payload[:html_response] = capture.html_response?
153
+ payload[:response_successful] = capture.successful?
154
+ payload[:html_bytes] = capture.html.bytesize
155
+ payload[:graph_frames] = capture.recorder.graph.frame_nodes.size
156
+ payload[:graph_dependencies] = capture.recorder.graph.dependency_nodes.size
157
+ capture.timings.each do |phase, ms|
158
+ payload[:"capture_#{phase}"] = ms
159
+ end
160
+ capture.counters.each do |counter, value|
161
+ payload[:"capture_#{counter}"] = value
162
+ end
163
+ end
164
+
165
+ def measure_phase(payload, key)
166
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
167
+ yield
168
+ ensure
169
+ payload[key] = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0).round(3)
170
+ end
171
+
172
+ def upkeep_subscription_request?
173
+ upkeep_reactive_request? && request.format.html? && (request.get? || request.head?)
174
+ end
175
+
176
+ def upkeep_reactive_continuation_request?
177
+ request.headers["Turbo-Frame"].present? &&
178
+ request.headers[ClientSubscription::ID_HEADER].present? &&
179
+ request.headers[ClientSubscription::TOKEN_HEADER].present?
180
+ end
181
+
182
+ def request_capture_profile?
183
+ ActiveSupport::Notifications.notifier.listening?(Upkeep::Rails::REQUEST_CAPTURE)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ module Install
6
+ module_function
7
+
8
+ def call
9
+ return unless Upkeep::Rails.configuration.enabled
10
+
11
+ Runtime::Install.call if defined?(::ActiveRecord::Base)
12
+ ActionViewCapture.install if defined?(::ActionView::Template)
13
+ ControllerRuntime.install if defined?(::ActionController::Base)
14
+ JobRuntime.install if defined?(::ActiveJob::Base)
15
+
16
+ @installed = true
17
+ end
18
+
19
+ def installed?
20
+ !!@installed
21
+ end
22
+
23
+ def reset!
24
+ @installed = false
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/concern"
4
+
5
+ module Upkeep
6
+ module Rails
7
+ module JobRuntime
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ around_perform :upkeep_capture_job
12
+ end
13
+
14
+ module_function
15
+
16
+ def install
17
+ return if @installed
18
+ return unless defined?(::ActiveJob::Base)
19
+
20
+ ::ActiveJob::Base.include(self)
21
+ @installed = true
22
+ end
23
+
24
+ def installed?
25
+ !!@installed
26
+ end
27
+
28
+ def reset!
29
+ @installed = false
30
+ end
31
+
32
+ private
33
+
34
+ def upkeep_capture_job
35
+ return yield if Upkeep::Runtime::ChangeLog.capturing?
36
+
37
+ result, changes = Upkeep::Runtime::ChangeLog.capture { yield }
38
+ Upkeep::Rails.deliver_changes!(changes)
39
+ result
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Rails
5
+ class Railtie < ::Rails::Railtie
6
+ config.upkeep = ActiveSupport::OrderedOptions.new
7
+
8
+ initializer "upkeep_rails.configure" do |app|
9
+ Upkeep::Rails.configure do |config|
10
+ config.enabled = app.config.upkeep.fetch(:enabled, true)
11
+ config.subscription_store = app.config.upkeep.fetch(:subscription_store, config.subscription_store)
12
+ config.deliver_inline = app.config.upkeep.fetch(:deliver_inline, config.deliver_inline)
13
+ config.delivery_batch_window =
14
+ app.config.upkeep.fetch(:delivery_batch_window, config.delivery_batch_window)
15
+ config.activation_token_expires_in =
16
+ app.config.upkeep.fetch(:activation_token_expires_in, config.activation_token_expires_in)
17
+ config.request_activation =
18
+ app.config.upkeep.fetch(:request_activation, config.request_activation)
19
+ config.refused_boundary_behavior =
20
+ app.config.upkeep.fetch(:refused_boundary_behavior, config.refused_boundary_behavior)
21
+ end
22
+ end
23
+
24
+ initializer "upkeep_rails.install", after: "turbo.helpers" do
25
+ ActiveSupport.on_load(:active_record) { Upkeep::Rails::Install.call }
26
+ ActiveSupport.on_load(:active_job) { Upkeep::Rails::Install.call }
27
+ ActiveSupport.on_load(:action_controller_base) { Upkeep::Rails::Install.call }
28
+ ActiveSupport.on_load(:action_view) { Upkeep::Rails::Install.call }
29
+ end
30
+
31
+ initializer "upkeep_rails.validate_configuration", after: "upkeep_rails.install" do |app|
32
+ app.config.after_initialize do
33
+ Upkeep::Rails.validate_configuration! unless Railtie.rake_task?
34
+ end
35
+ end
36
+
37
+ def self.rake_task?
38
+ defined?(::Rake) &&
39
+ ::Rake.respond_to?(:application) &&
40
+ ::Rake.application.top_level_tasks.any?
41
+ end
42
+ end
43
+ end
44
+ end