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,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+ require "active_support/notifications"
6
+ require_relative "active_registry"
7
+ require_relative "base_store"
8
+ require_relative "lookup_instrumentation"
9
+
10
+ module Upkeep
11
+ module Subscriptions
12
+ class NotFound < KeyError; end
13
+
14
+ Subscription = Data.define(:id, :subscriber_id, :recorder, :graph, :metadata) do
15
+ def explain
16
+ dependency_nodes = graph.dependency_nodes
17
+ dependencies = dependency_nodes.map(&:payload)
18
+
19
+ {
20
+ id: id,
21
+ subscriber_id: subscriber_id,
22
+ tables: active_record_tables(dependencies),
23
+ identity: identity_dependencies(dependencies),
24
+ frame_count: graph.frame_nodes.size,
25
+ dependency_count: dependency_nodes.size,
26
+ lookup_keys: lookup_keys_for(dependencies),
27
+ metadata: explain_metadata
28
+ }
29
+ end
30
+
31
+ def identity_signature(frame_id)
32
+ recorder.identity_signature(frame_id)
33
+ end
34
+
35
+ def replay_recipe(frame_id)
36
+ graph.node(frame_id).payload[:recipe]
37
+ end
38
+
39
+ def to_h
40
+ {
41
+ id: id,
42
+ subscriber_id: subscriber_id,
43
+ recorder: recorder.to_h,
44
+ metadata: metadata
45
+ }
46
+ end
47
+
48
+ def to_persistent_h
49
+ {
50
+ id: id,
51
+ subscriber_id: subscriber_id,
52
+ recorder: recorder.to_persistent_h,
53
+ metadata: metadata
54
+ }
55
+ end
56
+
57
+ def self.from_h(snapshot)
58
+ snapshot = Dependencies.symbolize_keys(snapshot)
59
+ recorder = Runtime::Recorder.from_h(snapshot.fetch(:recorder))
60
+
61
+ new(
62
+ snapshot.fetch(:id),
63
+ snapshot.fetch(:subscriber_id),
64
+ recorder,
65
+ recorder.graph,
66
+ snapshot.fetch(:metadata)
67
+ )
68
+ end
69
+
70
+ private
71
+
72
+ def active_record_tables(dependencies)
73
+ tables = Hash.new { |hash, table| hash[table] = [] }
74
+
75
+ dependencies.each do |dependency|
76
+ case dependency.source
77
+ when :active_record_attribute
78
+ tables[dependency.key.fetch(:table).to_s] << dependency.key.fetch(:attribute).to_s
79
+ when :active_record_collection, :active_record_query
80
+ dependency.metadata.fetch(:table_columns, {}).each do |table, columns|
81
+ tables[table.to_s].concat(Array(columns).map(&:to_s))
82
+ end
83
+ end
84
+ end
85
+
86
+ tables.transform_values { |columns| columns.uniq.sort }.sort.to_h
87
+ end
88
+
89
+ def identity_dependencies(dependencies)
90
+ dependencies.select(&:identity?).map do |dependency|
91
+ {
92
+ source: dependency.source.to_s,
93
+ key: dependency.key.fetch(:key),
94
+ value: dependency.key.fetch(:value),
95
+ partitioning: Dependencies.partitioning_identity?(dependency)
96
+ }
97
+ end.sort_by(&:inspect)
98
+ end
99
+
100
+ def lookup_keys_for(dependencies)
101
+ index = ReverseIndex.new
102
+ dependencies.flat_map { |dependency| index.lookup_keys_for_dependency(dependency) }.uniq.sort_by(&:inspect)
103
+ end
104
+
105
+ def explain_metadata
106
+ keys = [
107
+ :path,
108
+ "path",
109
+ :stream_name,
110
+ "stream_name",
111
+ :shared_stream_names,
112
+ "shared_stream_names",
113
+ :identity_mode,
114
+ "identity_mode",
115
+ :identity_sources,
116
+ "identity_sources",
117
+ :identity_names,
118
+ "identity_names",
119
+ :subscription_shape_key,
120
+ "subscription_shape_key"
121
+ ]
122
+
123
+ metadata.to_h.select { |key, _value| keys.include?(key) }
124
+ end
125
+ end
126
+
127
+ class MemoryReverseIndex
128
+ include LookupInstrumentation
129
+
130
+ LOOKUP_NOTIFICATION = LookupInstrumentation::LOOKUP_NOTIFICATION
131
+
132
+ def initialize(active_registry:, pending_registry:)
133
+ @active_registry = active_registry
134
+ @pending_registry = pending_registry
135
+ end
136
+
137
+ def summary
138
+ active_registry.summary
139
+ end
140
+
141
+ private
142
+
143
+ attr_reader :active_registry, :pending_registry
144
+
145
+ def lookup_store
146
+ "memory"
147
+ end
148
+
149
+ def entries_for_without_payload(changes)
150
+ active_registry.entries_for(changes)
151
+ end
152
+
153
+ def entries_for_with_payload(changes, payload)
154
+ active_entries = active_registry.entries_for(changes)
155
+ pending_entries = pending_registry.entries_for(changes)
156
+
157
+ payload.merge!(
158
+ active_entries: active_entries.size,
159
+ active_subscriptions: active_registry.count,
160
+ pending_entries: pending_entries.size,
161
+ pending_subscriptions: pending_registry.count,
162
+ persistent_entries: 0,
163
+ persistent_direct_index_entries: 0,
164
+ persistent_shape_index_entries: 0,
165
+ persistent_direct_lookup_keys: 0,
166
+ persistent_shape_lookup_keys: 0,
167
+ persistent_shape_keys: 0,
168
+ persistent_shape_subscriptions: 0,
169
+ mode: active_entries.empty? && pending_entries.any? ? "pending_activation" : "active"
170
+ )
171
+
172
+ payload[:miss_reason] = active_entries.empty? ? miss_reason(pending_entries) : nil
173
+ payload.delete(:miss_reason) unless payload[:miss_reason]
174
+
175
+ active_entries
176
+ end
177
+ end
178
+
179
+ class Store < BaseStore
180
+ PERSIST_NOTIFICATION = "persist_subscription_store.upkeep"
181
+
182
+ attr_reader :reverse_index
183
+
184
+ def initialize(reverse_index: ReverseIndex.new)
185
+ @active_registry = ActiveRegistry.new(reverse_index: reverse_index)
186
+ @pending_registry = ActiveRegistry.new
187
+ @pending_index_entries = {}
188
+ @reverse_index = MemoryReverseIndex.new(active_registry: active_registry, pending_registry: pending_registry)
189
+ @next_id = 0
190
+ end
191
+
192
+ def register(subscriber_id:, recorder:, metadata: {}, entries: nil)
193
+ subscription = with_optional_notification(PERSIST_NOTIFICATION, memory_persist_payload(operation: :persist_subscription)) do |payload|
194
+ register_subscription(subscriber_id: subscriber_id, recorder: recorder, metadata: metadata, entries: entries, payload: payload)
195
+ end
196
+ trim_opportunistically
197
+ subscription
198
+ end
199
+
200
+ def prune_stale!(older_than: stale_threshold, limit: nil)
201
+ stale_ids = subscriptions.filter_map do |subscription|
202
+ id = subscription.id
203
+ id if last_seen_at(subscription) && last_seen_at(subscription) < older_than
204
+ end
205
+ stale_ids = stale_ids.first(limit) if limit
206
+
207
+ unregister(stale_ids)
208
+ stale_ids.size
209
+ end
210
+
211
+ def activate(id)
212
+ with_optional_notification(PERSIST_NOTIFICATION, memory_persist_payload(operation: :persist_index)) do |payload|
213
+ activate_subscription(id, payload: payload)
214
+ end
215
+ end
216
+
217
+ def shutdown
218
+ true
219
+ end
220
+
221
+ def subscriptions
222
+ active_registry.subscriptions + pending_registry.subscriptions
223
+ end
224
+
225
+ def reset
226
+ @active_registry = ActiveRegistry.new
227
+ @pending_registry = ActiveRegistry.new
228
+ @pending_index_entries = {}
229
+ @reverse_index = MemoryReverseIndex.new(active_registry: active_registry, pending_registry: pending_registry)
230
+ @next_id = 0
231
+ end
232
+
233
+ def summary
234
+ active = active_registry.summary
235
+ pending = pending_registry.summary
236
+ {
237
+ subscriptions: subscriptions.size,
238
+ pending_subscriptions: pending_registry.count,
239
+ active_subscriptions: active_registry.count,
240
+ deferred_index_subscriptions: 0,
241
+ reverse_index: active.merge(
242
+ mode: :active,
243
+ active: active,
244
+ pending: pending,
245
+ persistent: { lookup_keys: 0, entries: 0 }
246
+ )
247
+ }
248
+ end
249
+
250
+ private
251
+
252
+ attr_reader :pending_registry, :active_registry
253
+
254
+ def after_touch(id, metadata:, now:)
255
+ true
256
+ end
257
+
258
+ def store_label
259
+ "memory"
260
+ end
261
+
262
+ def before_unregister(ids)
263
+ ids.each { |id| @pending_index_entries.delete(id) }
264
+ end
265
+
266
+ def fetch_missing(id)
267
+ raise NotFound, id
268
+ end
269
+
270
+ def register_subscription(subscriber_id:, recorder:, metadata: {}, entries: nil, payload: nil)
271
+ recorder.flush_pending_dependencies if recorder.respond_to?(:flush_pending_dependencies)
272
+ subscription = Subscription.new(
273
+ next_subscription_id,
274
+ subscriber_id,
275
+ recorder,
276
+ recorder.graph,
277
+ metadata
278
+ )
279
+
280
+ if payload
281
+ entry_count = dependency_entry_count(subscription, entries)
282
+ payload[:pending_index_entries] = entry_count
283
+ payload[:subscription_rows] = 1
284
+ payload[:index_rows] = 0
285
+ payload[:direct_index_rows] = 0
286
+ payload[:shape_index_rows] = 0
287
+ end
288
+
289
+ pending_registry.register(subscription, entries: entries)
290
+ @pending_index_entries[subscription.id] = entries if entries
291
+ subscription
292
+ end
293
+
294
+ def activate_subscription(id, payload: nil)
295
+ if active_registry.fetch(id)
296
+ payload.merge!(subscription_rows: 0, index_rows: 0, direct_index_rows: 0, shape_index_rows: 0) if payload
297
+ return true
298
+ end
299
+
300
+ subscription = pending_registry.fetch(id)
301
+ unless subscription
302
+ payload.merge!(subscription_rows: 0, index_rows: 0, direct_index_rows: 0, shape_index_rows: 0) if payload
303
+ return false
304
+ end
305
+
306
+ entries = @pending_index_entries.delete(id)
307
+ if payload
308
+ entry_count = dependency_entry_count(subscription, entries)
309
+ payload[:dependency_entries] = entry_count
310
+ payload[:subscription_rows] = 0
311
+ payload[:index_rows] = entry_count
312
+ payload[:direct_index_rows] = entry_count
313
+ payload[:shape_index_rows] = 0
314
+ end
315
+
316
+ active_registry.register(subscription, entries: entries)
317
+ pending_registry.unregister(id)
318
+ true
319
+ end
320
+
321
+ def memory_persist_payload(operation:)
322
+ {
323
+ store: "memory",
324
+ jobs: 1,
325
+ subscriptions: operation == :persist_subscription ? 1 : 0,
326
+ index_jobs: operation == :persist_index ? 1 : 0,
327
+ dependency_entries: 0,
328
+ pending_index_entries: 0,
329
+ operations: { operation.to_s => 1 }
330
+ }
331
+ end
332
+
333
+ def dependency_entry_count(subscription, entries)
334
+ index = ReverseIndex.new
335
+ materialized = if entries
336
+ index.entries_for_subscription_instance(entries, subscription)
337
+ else
338
+ index.entries_for_subscription(subscription)
339
+ end
340
+ materialized.uniq { |entry| [entry.owner_id, entry.dependency_cache_key] }.size
341
+ end
342
+
343
+ def last_seen_at(subscription)
344
+ value = subscription.metadata["last_seen_at"] || subscription.metadata[:last_seen_at]
345
+ Time.parse(value.to_s) if value
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "subscriptions/reverse_index"
4
+ require_relative "subscriptions/shape"
5
+ require_relative "subscriptions/registrar"
6
+ require_relative "subscriptions/store"
7
+ require_relative "subscriptions/active_record_store"
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "nokogiri"
5
+
6
+ module Upkeep
7
+ module Targeting
8
+ Target = Data.define(:kind, :id, :reason)
9
+ Patch = Data.define(:target, :html)
10
+
11
+ class Selector
12
+ def select(recorder, changes)
13
+ graph = recorder.graph
14
+
15
+ frame_nodes =
16
+ graph.dependency_node_ids_matching(changes)
17
+ .flat_map { |dependency_id| graph.dependency_owner_ids(dependency_id) }
18
+ .flat_map { |owner_id| graph.nearest_frame_nodes_from(owner_id) }
19
+
20
+ uniq_targets(remove_contained_frames(graph, frame_nodes).filter_map { |frame| target_for_frame(frame) })
21
+ end
22
+
23
+ private
24
+
25
+ def remove_contained_frames(graph, frames)
26
+ frames.uniq(&:id).reject do |frame|
27
+ frames.any? { |candidate| candidate.id != frame.id && graph.contained_by?(frame.id, candidate.id) }
28
+ end
29
+ end
30
+
31
+ def target_for_frame(frame)
32
+ case frame.payload.fetch(:kind)
33
+ when "page"
34
+ Target.new("page", frame.id, "page frame dependency matched committed change")
35
+ when "render_site"
36
+ Target.new("render_site", frame.payload.fetch(:site_id), "render-site dependency matched committed change")
37
+ when "fragment"
38
+ Target.new("fragment", frame.id, "record attribute read matched committed attributes")
39
+ when "turbo_frame"
40
+ Target.new("turbo_frame", frame.payload.fetch(:target_id), "Turbo Frame dependency matched committed change")
41
+ end
42
+ end
43
+
44
+ def uniq_targets(targets)
45
+ targets.uniq { |target| [target.kind, target.id] }
46
+ end
47
+ end
48
+
49
+ class Patcher
50
+ def initialize(html)
51
+ @fragment = Extraction.parse_html(html)
52
+ end
53
+
54
+ def apply(patches)
55
+ patches.each { |patch| apply_patch(patch) }
56
+ fragment.to_html
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :fragment
62
+
63
+ def apply_patch(patch)
64
+ node = node_for(patch.target)
65
+ raise "target not found in current DOM: #{patch.target.inspect}" unless node
66
+
67
+ replacement = replacement_for(patch)
68
+ node.replace(replacement)
69
+ end
70
+
71
+ def node_for(target)
72
+ Extraction.node_for(fragment, target)
73
+ end
74
+
75
+ def replacement_for(patch)
76
+ parsed = Extraction.parse_html(patch.html)
77
+ Extraction.node_for(parsed, patch.target) ||
78
+ parsed.children.find { |child| child.element? } ||
79
+ parsed.at_css("body > *")
80
+ end
81
+ end
82
+
83
+ module Extraction
84
+ module_function
85
+
86
+ def patches_from_full_rerender(full_html, targets)
87
+ targets.map { |target| Patch.new(target, extract_target_html(full_html, target)) }
88
+ end
89
+
90
+ def extract_target_html(html, target)
91
+ fragment = parse_html(html)
92
+ node = node_for(fragment, target)
93
+
94
+ raise "target not found in full rerender: #{target.inspect}" unless node
95
+
96
+ node.to_html
97
+ end
98
+
99
+ def node_for(fragment, target)
100
+ case target.kind
101
+ when "page"
102
+ fragment.at_css(%([data-upkeep-page-frame="#{css_escape(target.id)}"]))
103
+ when "fragment"
104
+ fragment.at_css(%([data-upkeep-frame="#{css_escape(target.id)}"]))
105
+ when "render_site"
106
+ fragment.at_css(%([data-upkeep-render-site="#{css_escape(target.id)}"]))
107
+ when "turbo_frame"
108
+ fragment.at_css(%(turbo-frame[id="#{css_escape(target.id)}"]))
109
+ end
110
+ end
111
+
112
+ def normalize_html(html)
113
+ parse_html(html).to_html
114
+ end
115
+
116
+ def digest_html(html)
117
+ Digest::SHA256.hexdigest(normalize_html(html))
118
+ end
119
+
120
+ def frame_id_for(target)
121
+ case target.kind
122
+ when "render_site"
123
+ "site:#{target.id}"
124
+ when "turbo_frame"
125
+ "turbo_frame:#{target.id}"
126
+ else
127
+ target.id
128
+ end
129
+ end
130
+
131
+ def css_escape(value)
132
+ value.to_s.gsub("\\", "\\\\\\").gsub('"', '\"')
133
+ end
134
+
135
+ def parse_html(html)
136
+ source = html.to_s
137
+
138
+ if source.match?(/\A\s*(?:<!doctype\b[^>]*>\s*)?<html[\s>]/i)
139
+ Nokogiri::HTML5.parse(source)
140
+ else
141
+ Nokogiri::HTML5.fragment(source)
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ VERSION = "0.2.5"
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "upkeep"
data/lib/upkeep.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "upkeep/version"
4
+ require_relative "upkeep/dependencies"
5
+ require_relative "upkeep/dag"
6
+ require_relative "upkeep/replay"
7
+ require_relative "upkeep/sql_dependency_analysis"
8
+ require_relative "upkeep/active_record_query"
9
+ require_relative "upkeep/runtime"
10
+ require_relative "upkeep/targeting"
11
+ require_relative "upkeep/subscriptions"
12
+ require_relative "upkeep/shared_streams"
13
+ require_relative "upkeep/invalidation"
14
+ require_relative "upkeep/delivery"
15
+ require_relative "upkeep/rails"
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/upkeep/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "upkeep-rails"
7
+ spec.version = Upkeep::VERSION
8
+ spec.authors = [ "Felipe dos Anjos" ]
9
+ spec.email = [ "felipe.cavalheiro.anjos@gmail.com" ]
10
+ spec.license = "MIT"
11
+
12
+ spec.summary = "Dependency-tracked live updates for Rails views"
13
+ spec.description = "Upkeep records the data and identity dependencies used while Rails renders a view, then updates subscribed frames when matching application data changes."
14
+ spec.homepage = "https://github.com/fc-anjos/upkeep-rails"
15
+ spec.required_ruby_version = ">= 3.2.0"
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/v#{spec.version}"
18
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
19
+ spec.metadata["rubygems_mfa_required"] = "true"
20
+
21
+ internal_files = Dir[
22
+ "lib/upkeep/probes/**/*.rb",
23
+ "lib/upkeep/proofs/**/*.rb"
24
+ ] + %w[
25
+ lib/upkeep/domain.rb
26
+ lib/upkeep/herb/fallback_analyzer.rb
27
+ lib/upkeep/herb/performance_gate.rb
28
+ lib/upkeep/herb/runtime_alignment.rb
29
+ lib/upkeep/proof_support.rb
30
+ lib/upkeep/rendering.rb
31
+ lib/upkeep/templates.rb
32
+ ]
33
+
34
+ native_platform = ENV["UPKEEP_NATIVE_PLATFORM"]
35
+ spec.files = (Dir[
36
+ "README.md",
37
+ "docs/**/*.md",
38
+ "LICENSE.txt",
39
+ "upkeep-rails.gemspec",
40
+ "lib/**/*.rb",
41
+ "lib/generators/**/templates/**/*"
42
+ ] - internal_files).sort
43
+
44
+ if native_platform
45
+ native_libraries = Dir[
46
+ "lib/upkeep/sqlglot/{lib,}sqlglot_rust.{so,dylib,dll}"
47
+ ]
48
+ raise "native library is required for platform gem #{native_platform}" if native_libraries.empty?
49
+
50
+ spec.platform = native_platform
51
+ spec.files.concat(native_libraries)
52
+ end
53
+
54
+ spec.require_paths = [ "lib" ]
55
+
56
+ spec.add_dependency "actionview", ">= 7.1", "< 9.0"
57
+ spec.add_dependency "actionpack", ">= 7.1", "< 9.0"
58
+ spec.add_dependency "actioncable", ">= 7.1", "< 9.0"
59
+ spec.add_dependency "activerecord", ">= 7.1", "< 9.0"
60
+ spec.add_dependency "activesupport", ">= 7.1", "< 9.0"
61
+ spec.add_dependency "herb", ">= 0.10.1", "< 0.11"
62
+ spec.add_dependency "ffi", ">= 1.15", "< 2.0"
63
+ spec.add_dependency "nokogiri", ">= 1.15", "< 2.0"
64
+ spec.add_dependency "railties", ">= 7.1", "< 9.0"
65
+ spec.add_dependency "turbo-rails", ">= 2.0", "< 3.0"
66
+ end