upkeep-rails 0.1.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.
Potentially problematic release.
This version of upkeep-rails might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +424 -0
- data/docs/architecture/ambient-inputs-roadmap.md +306 -0
- data/docs/architecture/herb-roadmap.md +324 -0
- data/docs/architecture/identity-and-sharing.md +187 -0
- data/docs/architecture/query-dependencies.md +230 -0
- data/docs/cost-model-roadmap.md +703 -0
- data/docs/guides/getting-started.md +282 -0
- data/docs/handoff-2026-05-15.md +230 -0
- data/docs/production_roadmap.md +372 -0
- data/docs/shared-warm-scale-roadmap.md +214 -0
- data/docs/single-subscriber-cold-roadmap.md +192 -0
- data/docs/testing.md +113 -0
- data/lib/generators/upkeep/install/install_generator.rb +90 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +31 -0
- data/lib/generators/upkeep/install/templates/subscription.js +107 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +6 -0
- data/lib/upkeep/active_record_query.rb +294 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +370 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +275 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +466 -0
- data/lib/upkeep/herb/developer_report.rb +116 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +84 -0
- data/lib/upkeep/herb/template_manifest.rb +377 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +341 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +765 -0
- data/lib/upkeep/rails/cable/channel.rb +108 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +214 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +37 -0
- data/lib/upkeep/rails/configuration.rb +57 -0
- data/lib/upkeep/rails/controller_runtime.rb +137 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/railtie.rb +36 -0
- data/lib/upkeep/rails/replay.rb +176 -0
- data/lib/upkeep/rails/testing.rb +36 -0
- data/lib/upkeep/rails.rb +276 -0
- data/lib/upkeep/replay.rb +408 -0
- data/lib/upkeep/runtime.rb +1075 -0
- data/lib/upkeep/shared_streams.rb +72 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +292 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +291 -0
- data/lib/upkeep/subscriptions/active_registry.rb +93 -0
- data/lib/upkeep/subscriptions/async_durable_writer.rb +136 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +122 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +144 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +294 -0
- data/lib/upkeep/subscriptions/shape.rb +116 -0
- data/lib/upkeep/subscriptions/store.rb +159 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +135 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +14 -0
- data/upkeep-rails.gemspec +53 -0
- metadata +296 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_controller"
|
|
4
|
+
require "active_support/hash_with_indifferent_access"
|
|
5
|
+
require "stringio"
|
|
6
|
+
|
|
7
|
+
module Upkeep
|
|
8
|
+
module Rails
|
|
9
|
+
module Replay
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
class RackSession < ActiveSupport::HashWithIndifferentAccess
|
|
13
|
+
def enabled? = true
|
|
14
|
+
|
|
15
|
+
def loaded? = true
|
|
16
|
+
|
|
17
|
+
def id = self[:session_id]
|
|
18
|
+
|
|
19
|
+
def id_was = id
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(recipe)
|
|
23
|
+
replay = recipe.replay
|
|
24
|
+
|
|
25
|
+
case replay
|
|
26
|
+
when ::Upkeep::Replay::ControllerPage
|
|
27
|
+
render_controller_page(replay)
|
|
28
|
+
when ::Upkeep::Replay::Template
|
|
29
|
+
render_template(replay)
|
|
30
|
+
when ::Upkeep::Replay::Fragment
|
|
31
|
+
render_fragment(replay)
|
|
32
|
+
when ::Upkeep::Replay::Collection
|
|
33
|
+
render_collection(replay)
|
|
34
|
+
when ::Upkeep::Replay::CollectionMember
|
|
35
|
+
render_collection_member(replay)
|
|
36
|
+
else
|
|
37
|
+
raise "unknown Rails replay recipe type: #{replay.class.name}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render_controller_page(replay)
|
|
42
|
+
controller = constantize(replay.controller_class)
|
|
43
|
+
_status, _headers, body = ControllerRuntime.suppress do
|
|
44
|
+
controller.action(replay.action).call(rack_env(replay.env))
|
|
45
|
+
end
|
|
46
|
+
collect_response_body(body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_template(replay)
|
|
50
|
+
renderer_for(replay).render(
|
|
51
|
+
template: replay.template,
|
|
52
|
+
locals: revive_hash(replay.locals)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def render_fragment(replay)
|
|
57
|
+
renderer_for(replay).render(
|
|
58
|
+
partial: partial_path(replay.template),
|
|
59
|
+
locals: revive_hash(replay.locals)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_collection(replay)
|
|
64
|
+
options = revive_hash(replay.options)
|
|
65
|
+
collection = revive_value(replay.collection)
|
|
66
|
+
|
|
67
|
+
if replay.derived_partial?
|
|
68
|
+
renderer_for(replay).render(collection)
|
|
69
|
+
else
|
|
70
|
+
renderer_for(replay).render(options.merge(
|
|
71
|
+
partial: replay.partial,
|
|
72
|
+
collection: collection
|
|
73
|
+
))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_collection_member(replay)
|
|
78
|
+
options = revive_hash(replay.options)
|
|
79
|
+
record = revive_value(replay.record)
|
|
80
|
+
locals = options.fetch(:locals, {})
|
|
81
|
+
local_name = (options[:as] || inferred_local_name(replay.partial)).to_sym
|
|
82
|
+
|
|
83
|
+
renderer_for(replay).render(
|
|
84
|
+
partial: replay.partial,
|
|
85
|
+
locals: locals.merge(local_name => record)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def renderer_for(replay)
|
|
90
|
+
if replay.controller_class
|
|
91
|
+
constantize(replay.controller_class).renderer
|
|
92
|
+
else
|
|
93
|
+
::ActionController::Base.renderer
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def rack_env(env)
|
|
98
|
+
env = env.each_with_object({}) { |(key, value), copy| copy[key.to_s] = revive_env_value(value) }
|
|
99
|
+
env["rack.input"] = StringIO.new
|
|
100
|
+
env["rack.errors"] ||= StringIO.new
|
|
101
|
+
env
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def revive_hash(values)
|
|
105
|
+
values = values.entries if values.is_a?(::Upkeep::Replay::HashValue)
|
|
106
|
+
|
|
107
|
+
values.each_with_object({}) do |(key, value), revived|
|
|
108
|
+
revived[key.to_sym] = revive_value(value)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def revive_value(snapshot)
|
|
113
|
+
snapshot = ::Upkeep::Replay.value(snapshot)
|
|
114
|
+
|
|
115
|
+
case snapshot
|
|
116
|
+
when ::Upkeep::Replay::ActiveRecordValue
|
|
117
|
+
constantize(snapshot.model).find(snapshot.id)
|
|
118
|
+
when ::Upkeep::Replay::ActiveRecordRelationValue
|
|
119
|
+
constantize(snapshot.model).find_by_sql(snapshot.sql)
|
|
120
|
+
when ::Upkeep::Replay::ArrayValue
|
|
121
|
+
snapshot.items.map { |item| revive_value(item) }
|
|
122
|
+
when ::Upkeep::Replay::HashValue
|
|
123
|
+
revive_hash(snapshot.entries)
|
|
124
|
+
when ::Upkeep::Replay::LiteralValue
|
|
125
|
+
snapshot.value
|
|
126
|
+
else
|
|
127
|
+
raise "unsupported Rails replay value type: #{snapshot.class.name}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def revive_env_value(value)
|
|
132
|
+
return revive_replay_session(value) if replay_session_snapshot?(value)
|
|
133
|
+
|
|
134
|
+
case value
|
|
135
|
+
when Hash
|
|
136
|
+
value.transform_values { |nested_value| revive_env_value(nested_value) }
|
|
137
|
+
when Array
|
|
138
|
+
value.map { |nested_value| revive_env_value(nested_value) }
|
|
139
|
+
else
|
|
140
|
+
value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def replay_session_snapshot?(value)
|
|
145
|
+
return false unless value.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
type = value["__upkeep_replay_type"] || value[:__upkeep_replay_type]
|
|
148
|
+
type == "rack_session"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def revive_replay_session(snapshot)
|
|
152
|
+
values = snapshot["values"] || snapshot[:values] || {}
|
|
153
|
+
RackSession.new(revive_env_value(values))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def partial_path(template)
|
|
157
|
+
template.to_s.sub(%r{(^|/)_([^/]+)\z}, "\\1\\2")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def inferred_local_name(partial)
|
|
161
|
+
File.basename(partial.to_s).sub(/\A_/, "").to_sym
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def constantize(name)
|
|
165
|
+
name.to_s.split("::").reduce(Object) { |namespace, constant_name| namespace.const_get(constant_name) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def collect_response_body(body)
|
|
169
|
+
body.each.to_a.join
|
|
170
|
+
ensure
|
|
171
|
+
body.close if body.respond_to?(:close)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Upkeep
|
|
4
|
+
module Rails
|
|
5
|
+
module Testing
|
|
6
|
+
def assert_upkeep_subscription_registered(message = nil)
|
|
7
|
+
assert_select "script[data-upkeep-subscription]"
|
|
8
|
+
assert Upkeep::Rails.subscriptions.subscriptions.any?,
|
|
9
|
+
message || "expected Upkeep to register at least one subscription"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def upkeep_subscription
|
|
13
|
+
Upkeep::Rails.subscriptions.subscriptions.last
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def upkeep_stream_names(subscription = upkeep_subscription)
|
|
17
|
+
raise ArgumentError, "no Upkeep subscription is registered" unless subscription
|
|
18
|
+
|
|
19
|
+
([subscription.metadata.fetch(:stream_name)] + subscription.metadata.fetch(:shared_stream_names, [])).uniq
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def capture_upkeep_broadcasts(subscription = upkeep_subscription, &block)
|
|
23
|
+
raise ArgumentError, "capture_upkeep_broadcasts requires a block" unless block
|
|
24
|
+
raise NoMethodError, "include ActionCable::TestHelper before calling capture_upkeep_broadcasts" unless respond_to?(:capture_broadcasts)
|
|
25
|
+
|
|
26
|
+
captures = {}
|
|
27
|
+
nested = upkeep_stream_names(subscription).reverse_each.reduce(block) do |inner, stream_name|
|
|
28
|
+
proc { captures[stream_name] = capture_broadcasts(stream_name, &inner) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
nested.call
|
|
32
|
+
captures.values.flatten
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/upkeep/rails.rb
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require_relative "capture/request"
|
|
5
|
+
require_relative "subscriptions/registrar"
|
|
6
|
+
require_relative "rails/configuration"
|
|
7
|
+
require_relative "rails/replay"
|
|
8
|
+
require_relative "rails/action_view_capture"
|
|
9
|
+
require_relative "rails/cable"
|
|
10
|
+
require_relative "rails/client_subscription"
|
|
11
|
+
require_relative "rails/controller_runtime"
|
|
12
|
+
require_relative "rails/install"
|
|
13
|
+
require_relative "rails/testing"
|
|
14
|
+
require_relative "rails/railtie" if defined?(::Rails::Railtie)
|
|
15
|
+
|
|
16
|
+
module Upkeep
|
|
17
|
+
module Rails
|
|
18
|
+
SUBSCRIPTION_IDENTITY = "upkeep.subscription_identity"
|
|
19
|
+
REQUEST_CAPTURE = "request_capture.upkeep"
|
|
20
|
+
INTERNAL_DELIVERY_TABLES = %w[
|
|
21
|
+
upkeep_subscriptions
|
|
22
|
+
upkeep_subscription_index_entries
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
class << self
|
|
26
|
+
def configuration
|
|
27
|
+
@configuration ||= Configuration.new
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configure
|
|
31
|
+
yield configuration
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def subscriptions
|
|
35
|
+
discard_subscription_store! if @subscriptions && subscription_store_config_changed?
|
|
36
|
+
@subscriptions ||= build_subscription_store
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def transport
|
|
40
|
+
@transport ||= Delivery::BroadcastTransport.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reset_runtime!
|
|
44
|
+
@delivery_dispatcher&.shutdown
|
|
45
|
+
@delivery_dispatcher = nil
|
|
46
|
+
@subscription_shape_cache&.reset
|
|
47
|
+
@subscription_registrar = nil
|
|
48
|
+
discard_subscription_store! if @subscriptions
|
|
49
|
+
@subscriptions = build_subscription_store
|
|
50
|
+
@subscriptions.reset
|
|
51
|
+
@transport = Delivery::BroadcastTransport.new
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def register_controller_subscription(controller, capture)
|
|
55
|
+
recorder = capture.recorder
|
|
56
|
+
return unless subscription_response?(controller, capture)
|
|
57
|
+
|
|
58
|
+
decision = Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder)
|
|
59
|
+
unless recorder.reactive?
|
|
60
|
+
instrument_subscription_identity(
|
|
61
|
+
decision,
|
|
62
|
+
registered: false,
|
|
63
|
+
deopt_reason: "refused_boundary",
|
|
64
|
+
refused_boundaries: recorder.refused_boundaries.map(&:reason)
|
|
65
|
+
)
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
identity = Cable::SubscriberIdentity.derive_from_request(
|
|
70
|
+
controller.request,
|
|
71
|
+
recorder: recorder,
|
|
72
|
+
decision: decision
|
|
73
|
+
)
|
|
74
|
+
registration = subscription_registrar.register(
|
|
75
|
+
identity: identity,
|
|
76
|
+
decision: decision,
|
|
77
|
+
recorder: recorder,
|
|
78
|
+
signature: capture.signature,
|
|
79
|
+
metadata: identity_metadata(decision).merge(
|
|
80
|
+
path: controller.request.fullpath,
|
|
81
|
+
stream_name: identity.stream_name
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
instrument_subscription_identity(decision, registered: true, subscription: registration.subscription)
|
|
85
|
+
|
|
86
|
+
registration
|
|
87
|
+
rescue Cable::UnidentifiedSubscriber => error
|
|
88
|
+
instrument_subscription_identity(
|
|
89
|
+
decision || Cable::SubscriberIdentity.decision_for(controller.request, recorder: recorder),
|
|
90
|
+
registered: false,
|
|
91
|
+
deopt_reason: "unidentified_identity",
|
|
92
|
+
error: error.message
|
|
93
|
+
)
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def deliver_changes!(changes = Runtime::ChangeLog.drain)
|
|
98
|
+
changes = deliverable_changes(changes)
|
|
99
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
100
|
+
|
|
101
|
+
delivery_dispatcher.enqueue(changes)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def deliver_changes_now!(changes = Runtime::ChangeLog.drain)
|
|
105
|
+
changes = deliverable_changes(changes)
|
|
106
|
+
return Delivery::Transport::DispatchReport.new([]) if changes.empty?
|
|
107
|
+
|
|
108
|
+
batch = delivery_batch_for([changes])
|
|
109
|
+
transport.deliver(batch)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def drain_delivery!
|
|
113
|
+
@delivery_dispatcher&.drain
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def validate_configuration!(environment: rails_environment)
|
|
117
|
+
return true unless configuration.enabled
|
|
118
|
+
|
|
119
|
+
validate_subscription_store!(environment: environment)
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def delivery_dispatcher
|
|
126
|
+
@delivery_dispatcher ||= Delivery::AsyncDispatcher.new do |change_sets|
|
|
127
|
+
batch = delivery_batch_for(change_sets)
|
|
128
|
+
transport.deliver(batch)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def subscription_registrar
|
|
133
|
+
@subscription_registrar ||= Subscriptions::Registrar.new(
|
|
134
|
+
store: subscriptions,
|
|
135
|
+
shape_cache: subscription_shape_cache
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def subscription_shape_cache
|
|
140
|
+
@subscription_shape_cache ||= Subscriptions::ShapeCache.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def delivery_batch_for(change_sets)
|
|
144
|
+
change_sets = compact_change_sets(change_sets)
|
|
145
|
+
return Delivery::TurboStreams::Batch.new([]) if change_sets.empty?
|
|
146
|
+
|
|
147
|
+
planner = Invalidation::Planner.new(store: subscriptions)
|
|
148
|
+
plans = change_sets.map { |changes| planner.plan(changes) }
|
|
149
|
+
plans = plans.reject { |plan| plan.targets.empty? }
|
|
150
|
+
return Delivery::TurboStreams::Batch.new([]) if plans.empty?
|
|
151
|
+
|
|
152
|
+
Delivery::TurboStreams.new.build_many(plans)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def compact_change_sets(change_sets)
|
|
156
|
+
change_sets
|
|
157
|
+
.map { |changes| deliverable_changes(changes) }
|
|
158
|
+
.reject(&:empty?)
|
|
159
|
+
.uniq { |changes| change_set_key(changes) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def deliverable_changes(changes)
|
|
163
|
+
Array(changes).reject { |change| internal_delivery_change?(change) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def internal_delivery_change?(change)
|
|
167
|
+
INTERNAL_DELIVERY_TABLES.include?(change_value(change, :table).to_s)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def change_set_key(changes)
|
|
171
|
+
changes.map { |change| change_key(change) }.sort
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def change_key(change)
|
|
175
|
+
[
|
|
176
|
+
change_value(change, :type).to_s,
|
|
177
|
+
change_value(change, :table).to_s,
|
|
178
|
+
change_value(change, :model).to_s,
|
|
179
|
+
change_value(change, :id).to_s,
|
|
180
|
+
Array(change_value(change, :changed_attributes)).map(&:to_s).sort,
|
|
181
|
+
change_value(change, :old_values).inspect,
|
|
182
|
+
change_value(change, :new_values).inspect
|
|
183
|
+
]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def change_value(change, key)
|
|
187
|
+
return unless change.respond_to?(:[])
|
|
188
|
+
|
|
189
|
+
change[key] || change[key.to_s]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def discard_subscription_store!
|
|
193
|
+
@subscriptions&.shutdown
|
|
194
|
+
@subscriptions = nil
|
|
195
|
+
@subscription_registrar = nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def identity_metadata(decision)
|
|
199
|
+
{
|
|
200
|
+
identity_mode: decision.mode,
|
|
201
|
+
anonymous: decision.anonymous,
|
|
202
|
+
anonymous_deopt_reason: decision.deopt_reason,
|
|
203
|
+
identity_sources: decision.identity_sources
|
|
204
|
+
}.compact
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def instrument_subscription_identity(decision, registered:, subscription: nil, deopt_reason: nil, **extra)
|
|
208
|
+
ActiveSupport::Notifications.instrument(
|
|
209
|
+
SUBSCRIPTION_IDENTITY,
|
|
210
|
+
{
|
|
211
|
+
registered: registered,
|
|
212
|
+
subscription_id: subscription&.id,
|
|
213
|
+
subscriber_id: subscription&.subscriber_id,
|
|
214
|
+
identity_mode: decision&.mode,
|
|
215
|
+
anonymous: decision&.anonymous,
|
|
216
|
+
anonymous_deopt_reason: deopt_reason || decision&.deopt_reason,
|
|
217
|
+
identity_sources: decision&.identity_sources
|
|
218
|
+
}.merge(extra)
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def subscription_response?(controller, capture)
|
|
223
|
+
controller.request.get? &&
|
|
224
|
+
capture.successful? &&
|
|
225
|
+
capture.html_response? &&
|
|
226
|
+
capture.html.include?("</") &&
|
|
227
|
+
capture.recorder.graph.frame_nodes.any?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_subscription_store
|
|
231
|
+
case configuration.subscription_store
|
|
232
|
+
when :active_record
|
|
233
|
+
unless Subscriptions::ActiveRecordStore.available?(connect: true)
|
|
234
|
+
raise ConfigurationError,
|
|
235
|
+
"Upkeep subscription_store=:active_record requires the upkeep_subscriptions and " \
|
|
236
|
+
"upkeep_subscription_index_entries tables. Run bin/rails generate upkeep:install " \
|
|
237
|
+
"and bin/rails db:migrate, or set config.upkeep.subscription_store = :memory in development/test."
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
Subscriptions::ActiveRecordStore.new
|
|
241
|
+
when :memory
|
|
242
|
+
Subscriptions::Store.new
|
|
243
|
+
end.tap do
|
|
244
|
+
@subscription_store_name = configuration.subscription_store
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def validate_subscription_store!(environment:)
|
|
249
|
+
if production_environment?(environment) && configuration.subscription_store == :memory
|
|
250
|
+
raise ConfigurationError,
|
|
251
|
+
"Upkeep subscription_store=:memory is only for development/test; production requires :active_record."
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
return true unless production_environment?(environment)
|
|
255
|
+
return true unless configuration.subscription_store == :active_record
|
|
256
|
+
return true if Subscriptions::ActiveRecordStore.available?(connect: true)
|
|
257
|
+
|
|
258
|
+
raise ConfigurationError,
|
|
259
|
+
"Upkeep production boot requires the upkeep_subscriptions and " \
|
|
260
|
+
"upkeep_subscription_index_entries tables for subscription_store=:active_record."
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def production_environment?(environment)
|
|
264
|
+
environment.to_s == "production"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def rails_environment
|
|
268
|
+
::Rails.env if defined?(::Rails) && ::Rails.respond_to?(:env)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def subscription_store_config_changed?
|
|
272
|
+
@subscription_store_name != configuration.subscription_store
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|