upkeep-rails 0.1.9

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +311 -0
  4. data/docs/how-it-works.md +269 -0
  5. data/lib/generators/upkeep/install/install_generator.rb +127 -0
  6. data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
  7. data/lib/generators/upkeep/install/templates/subscription.js +99 -0
  8. data/lib/generators/upkeep/install/templates/upkeep.rb +63 -0
  9. data/lib/upkeep/active_record_query.rb +392 -0
  10. data/lib/upkeep/capture/request.rb +150 -0
  11. data/lib/upkeep/dag/subscription_shape.rb +244 -0
  12. data/lib/upkeep/dag.rb +370 -0
  13. data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
  14. data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
  15. data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
  16. data/lib/upkeep/delivery/transport.rb +194 -0
  17. data/lib/upkeep/delivery/turbo_streams.rb +302 -0
  18. data/lib/upkeep/delivery.rb +7 -0
  19. data/lib/upkeep/dependencies.rb +550 -0
  20. data/lib/upkeep/herb/developer_report.rb +135 -0
  21. data/lib/upkeep/herb/manifest_cache.rb +83 -0
  22. data/lib/upkeep/herb/manifest_diff.rb +183 -0
  23. data/lib/upkeep/herb/source_instrumenter.rb +149 -0
  24. data/lib/upkeep/herb/template_manifest.rb +518 -0
  25. data/lib/upkeep/invalidation/collection_append.rb +84 -0
  26. data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
  27. data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
  28. data/lib/upkeep/invalidation/collection_remove.rb +57 -0
  29. data/lib/upkeep/invalidation/planner.rb +360 -0
  30. data/lib/upkeep/invalidation.rb +7 -0
  31. data/lib/upkeep/rails/action_view_capture.rb +920 -0
  32. data/lib/upkeep/rails/activation_token.rb +55 -0
  33. data/lib/upkeep/rails/cable/channel.rb +143 -0
  34. data/lib/upkeep/rails/cable/subscriber_identity.rb +341 -0
  35. data/lib/upkeep/rails/cable.rb +4 -0
  36. data/lib/upkeep/rails/client_subscription.rb +45 -0
  37. data/lib/upkeep/rails/configuration.rb +245 -0
  38. data/lib/upkeep/rails/controller_runtime.rb +154 -0
  39. data/lib/upkeep/rails/delivery_job.rb +29 -0
  40. data/lib/upkeep/rails/install.rb +28 -0
  41. data/lib/upkeep/rails/railtie.rb +50 -0
  42. data/lib/upkeep/rails/replay.rb +197 -0
  43. data/lib/upkeep/rails/testing.rb +258 -0
  44. data/lib/upkeep/rails.rb +370 -0
  45. data/lib/upkeep/replay.rb +439 -0
  46. data/lib/upkeep/runtime.rb +1202 -0
  47. data/lib/upkeep/shared_streams.rb +72 -0
  48. data/lib/upkeep/subscriptions/active_record_store.rb +387 -0
  49. data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +407 -0
  50. data/lib/upkeep/subscriptions/active_registry.rb +87 -0
  51. data/lib/upkeep/subscriptions/async_durable_writer.rb +131 -0
  52. data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
  53. data/lib/upkeep/subscriptions/layered_reverse_index.rb +129 -0
  54. data/lib/upkeep/subscriptions/persistent_reverse_index.rb +223 -0
  55. data/lib/upkeep/subscriptions/registrar.rb +36 -0
  56. data/lib/upkeep/subscriptions/reverse_index.rb +301 -0
  57. data/lib/upkeep/subscriptions/shape.rb +116 -0
  58. data/lib/upkeep/subscriptions/store.rb +375 -0
  59. data/lib/upkeep/subscriptions.rb +7 -0
  60. data/lib/upkeep/targeting.rb +135 -0
  61. data/lib/upkeep/version.rb +5 -0
  62. data/lib/upkeep-rails.rb +3 -0
  63. data/lib/upkeep.rb +14 -0
  64. data/upkeep-rails.gemspec +54 -0
  65. metadata +308 -0
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Upkeep
4
+ module Replay
5
+ module Payload
6
+ module_function
7
+
8
+ def from_h(snapshot)
9
+ return snapshot if snapshot.is_a?(Payload)
10
+
11
+ snapshot = Replay.symbolize_keys(snapshot || {})
12
+ return Empty.new if snapshot.empty?
13
+
14
+ case snapshot.fetch(:type).to_s
15
+ when "controller_page"
16
+ ControllerPage.new(
17
+ controller_class: snapshot[:controller_class],
18
+ action: snapshot.fetch(:action),
19
+ env: snapshot.fetch(:env)
20
+ )
21
+ when "template"
22
+ Template.new(
23
+ controller_class: snapshot[:controller_class],
24
+ template: snapshot.fetch(:template),
25
+ locals: Replay.value_hash_from_h(snapshot.fetch(:locals)),
26
+ assigns: Replay.value_hash_from_h(snapshot.fetch(:assigns, {}))
27
+ )
28
+ when "fragment"
29
+ Fragment.new(
30
+ controller_class: snapshot[:controller_class],
31
+ template: snapshot.fetch(:template),
32
+ locals: Replay.value_hash_from_h(snapshot.fetch(:locals)),
33
+ assigns: Replay.value_hash_from_h(snapshot.fetch(:assigns, {}))
34
+ )
35
+ when "collection"
36
+ Collection.new(
37
+ controller_class: snapshot[:controller_class],
38
+ partial: snapshot.fetch(:partial),
39
+ collection: Value.from_h(snapshot.fetch(:collection)),
40
+ options: Replay.value_hash_from_h(snapshot.fetch(:options))
41
+ )
42
+ when "collection_member"
43
+ CollectionMember.new(
44
+ controller_class: snapshot[:controller_class],
45
+ partial: snapshot.fetch(:partial),
46
+ record: Value.from_h(snapshot.fetch(:record)),
47
+ options: Replay.value_hash_from_h(snapshot.fetch(:options))
48
+ )
49
+ else
50
+ raise ArgumentError, "unknown replay payload type: #{snapshot.fetch(:type).inspect}"
51
+ end
52
+ end
53
+
54
+ def empty?
55
+ false
56
+ end
57
+ end
58
+
59
+ module Value
60
+ module_function
61
+
62
+ def from_h(snapshot)
63
+ return snapshot if snapshot.is_a?(Value)
64
+ return LiteralValue.new(value: snapshot) unless snapshot.is_a?(Hash)
65
+
66
+ snapshot = Replay.symbolize_keys(snapshot)
67
+
68
+ case snapshot.fetch(:type).to_s
69
+ when "active_record"
70
+ ActiveRecordValue.new(
71
+ model: snapshot.fetch(:model),
72
+ id: snapshot.fetch(:id)
73
+ )
74
+ when "active_record_relation"
75
+ ActiveRecordRelationValue.new(
76
+ model: snapshot.fetch(:model),
77
+ sql: snapshot.fetch(:sql),
78
+ primary_key: snapshot[:primary_key],
79
+ appendable: snapshot.fetch(:appendable),
80
+ limit_value: snapshot.fetch(:limit_value),
81
+ predicates: snapshot.fetch(:predicates),
82
+ member_ids: snapshot.fetch(:member_ids)
83
+ )
84
+ when "array"
85
+ ArrayValue.new(items: snapshot.fetch(:items).map { |item| from_h(item) })
86
+ when "hash"
87
+ HashValue.new(entries: Replay.value_hash_from_h(snapshot.fetch(:entries)))
88
+ when "literal"
89
+ LiteralValue.new(value: snapshot[:value])
90
+ when "rails_form_builder"
91
+ RailsFormBuilderValue.new(
92
+ builder_class: snapshot.fetch(:builder_class),
93
+ object_name: snapshot.fetch(:object_name),
94
+ object: Value.from_h(snapshot.fetch(:object)),
95
+ options: Replay.value_hash_from_h(snapshot.fetch(:options, {}))
96
+ )
97
+ when "unsupported"
98
+ UnsupportedValue.new(class_name: snapshot.fetch(:class))
99
+ when "refused_active_record_relation"
100
+ RefusedActiveRecordRelationValue.new(
101
+ model: snapshot.fetch(:model),
102
+ sql_digest: snapshot.fetch(:sql_digest),
103
+ reason: snapshot.fetch(:reason)
104
+ )
105
+ else
106
+ raise ArgumentError, "unknown replay value type: #{snapshot.fetch(:type).inspect}"
107
+ end
108
+ end
109
+ end
110
+
111
+ class Empty
112
+ include Payload
113
+
114
+ def empty?
115
+ true
116
+ end
117
+
118
+ def to_h
119
+ {}
120
+ end
121
+ end
122
+
123
+ class ControllerPage < Data.define(:controller_class, :action, :env)
124
+ include Payload
125
+
126
+ def type = "controller_page"
127
+
128
+ def to_h
129
+ {
130
+ type: type,
131
+ controller_class: controller_class,
132
+ action: action,
133
+ env: env
134
+ }.compact
135
+ end
136
+ end
137
+
138
+ class Template < Data.define(:controller_class, :template, :locals, :assigns)
139
+ include Payload
140
+
141
+ def type = "template"
142
+
143
+ def to_h
144
+ payload = {
145
+ type: type,
146
+ controller_class: controller_class,
147
+ template: template,
148
+ locals: Replay.value_hash_to_h(locals)
149
+ }.compact
150
+ replay_assigns = Replay.value_hash_to_h(assigns)
151
+ payload[:assigns] = replay_assigns if replay_assigns.any?
152
+ payload
153
+ end
154
+ end
155
+
156
+ class Fragment < Data.define(:controller_class, :template, :locals, :assigns)
157
+ include Payload
158
+
159
+ def type = "fragment"
160
+
161
+ def to_h
162
+ payload = {
163
+ type: type,
164
+ controller_class: controller_class,
165
+ template: template,
166
+ locals: Replay.value_hash_to_h(locals)
167
+ }.compact
168
+ replay_assigns = Replay.value_hash_to_h(assigns)
169
+ payload[:assigns] = replay_assigns if replay_assigns.any?
170
+ payload
171
+ end
172
+ end
173
+
174
+ class Collection < Data.define(:controller_class, :partial, :collection, :options)
175
+ include Payload
176
+
177
+ def type = "collection"
178
+
179
+ def derived_partial?
180
+ partial == "derived"
181
+ end
182
+
183
+ def to_h
184
+ {
185
+ type: type,
186
+ controller_class: controller_class,
187
+ partial: partial,
188
+ collection: collection.to_h,
189
+ options: Replay.value_hash_to_h(options)
190
+ }.compact
191
+ end
192
+ end
193
+
194
+ class CollectionMember < Data.define(:controller_class, :partial, :record, :options)
195
+ include Payload
196
+
197
+ def type = "collection_member"
198
+
199
+ def to_h
200
+ {
201
+ type: type,
202
+ controller_class: controller_class,
203
+ partial: partial,
204
+ record: record.to_h,
205
+ options: Replay.value_hash_to_h(options)
206
+ }.compact
207
+ end
208
+ end
209
+
210
+ class ActiveRecordValue < Data.define(:model, :id)
211
+ include Value
212
+
213
+ def type = "active_record"
214
+
215
+ def to_h
216
+ { type: type, model: model, id: id }
217
+ end
218
+ end
219
+
220
+ class ActiveRecordRelationValue < Data.define(:model, :sql, :primary_key, :appendable, :limit_value, :predicates, :member_ids)
221
+ include Value
222
+
223
+ def type = "active_record_relation"
224
+
225
+ def appendable?
226
+ !!appendable
227
+ end
228
+
229
+ def to_h
230
+ {
231
+ type: type,
232
+ model: model,
233
+ sql: sql,
234
+ primary_key: primary_key,
235
+ appendable: appendable,
236
+ limit_value: limit_value,
237
+ predicates: predicates,
238
+ member_ids: member_ids
239
+ }
240
+ end
241
+ end
242
+
243
+ class ArrayValue < Data.define(:items)
244
+ include Value
245
+
246
+ def type = "array"
247
+
248
+ def to_h
249
+ { type: type, items: items.map(&:to_h) }
250
+ end
251
+ end
252
+
253
+ class HashValue < Data.define(:entries)
254
+ include Value
255
+
256
+ def type = "hash"
257
+
258
+ def to_h
259
+ { type: type, entries: Replay.value_hash_to_h(entries) }
260
+ end
261
+ end
262
+
263
+ class LiteralValue < Data.define(:value)
264
+ include Value
265
+
266
+ def type = "literal"
267
+
268
+ def to_h
269
+ { type: type, value: value }
270
+ end
271
+ end
272
+
273
+ class RailsFormBuilderValue < Data.define(:builder_class, :object_name, :object, :options)
274
+ include Value
275
+
276
+ def type = "rails_form_builder"
277
+
278
+ def to_h
279
+ {
280
+ type: type,
281
+ builder_class: builder_class,
282
+ object_name: object_name,
283
+ object: Value.from_h(object).to_h,
284
+ options: Replay.value_hash_to_h(options)
285
+ }
286
+ end
287
+ end
288
+
289
+ class UnsupportedValue < Data.define(:class_name)
290
+ include Value
291
+
292
+ def type = "unsupported"
293
+
294
+ def to_h
295
+ { type: type, class: class_name }
296
+ end
297
+ end
298
+
299
+ class RefusedActiveRecordRelationValue < Data.define(:model, :sql_digest, :reason)
300
+ include Value
301
+
302
+ def type = "refused_active_record_relation"
303
+
304
+ def to_h
305
+ {
306
+ type: type,
307
+ model: model,
308
+ sql_digest: sql_digest,
309
+ reason: reason
310
+ }
311
+ end
312
+ end
313
+
314
+ module_function
315
+
316
+ def payload(value)
317
+ Payload.from_h(value)
318
+ end
319
+
320
+ def value(value)
321
+ Value.from_h(value)
322
+ end
323
+
324
+ def value_hash_from_h(values)
325
+ values.to_h.each_with_object({}) do |(key, nested_value), snapshot|
326
+ snapshot[key.to_s] = Value.from_h(nested_value)
327
+ end
328
+ end
329
+
330
+ def value_hash_to_h(values)
331
+ values.to_h.each_with_object({}) do |(key, nested_value), snapshot|
332
+ snapshot[key] = Value.from_h(nested_value).to_h
333
+ end
334
+ end
335
+
336
+ def active_record_value(record)
337
+ ActiveRecordValue.new(model: record.class.name, id: record.id)
338
+ end
339
+
340
+ class Recipe
341
+ attr_reader :kind, :frame_id, :target_kind, :target_id, :template, :metadata, :runtime, :replay
342
+
343
+ def initialize(kind:, frame_id:, target_kind:, target_id:, template: nil, metadata: {}, runtime: nil, replay: nil, &renderer)
344
+ @kind = kind
345
+ @frame_id = frame_id
346
+ @target_kind = target_kind
347
+ @target_id = target_id
348
+ @template = template
349
+ @metadata = metadata
350
+ @runtime = runtime
351
+ @replay = Replay.payload(replay)
352
+ @renderer = renderer
353
+ end
354
+
355
+ def render
356
+ return @renderer.call if @renderer
357
+
358
+ runtime_renderer.render(self)
359
+ end
360
+
361
+ def render_target(target)
362
+ html = render
363
+ return html if target_match?(target)
364
+
365
+ require_relative "targeting"
366
+ Targeting::Extraction.extract_target_html(html, target)
367
+ end
368
+
369
+ def target_match?(target)
370
+ target && target.kind != "page" && target.kind == target_kind && target.id == target_id
371
+ end
372
+
373
+ def manifest_target_render?(target)
374
+ !!manifest_reference && target_match?(target)
375
+ end
376
+
377
+ def manifest_reference
378
+ metadata[:manifest] || metadata["manifest"]
379
+ end
380
+
381
+ def to_h
382
+ snapshot = {
383
+ kind: kind,
384
+ frame_id: frame_id,
385
+ target_kind: target_kind,
386
+ target_id: target_id,
387
+ template: template,
388
+ metadata: metadata
389
+ }.compact
390
+
391
+ snapshot[:runtime] = runtime if runtime
392
+ replay_snapshot = replay&.to_h
393
+ snapshot[:replay] = replay_snapshot if replay_snapshot && !replay_snapshot.empty?
394
+ snapshot
395
+ end
396
+
397
+ def self.from_h(snapshot)
398
+ snapshot = Replay.symbolize_keys(snapshot)
399
+
400
+ new(
401
+ kind: snapshot.fetch(:kind),
402
+ frame_id: snapshot.fetch(:frame_id),
403
+ target_kind: snapshot.fetch(:target_kind),
404
+ target_id: snapshot.fetch(:target_id),
405
+ template: snapshot[:template],
406
+ metadata: snapshot.fetch(:metadata),
407
+ runtime: snapshot[:runtime],
408
+ replay: snapshot.fetch(:replay, {})
409
+ )
410
+ end
411
+
412
+ private
413
+
414
+ def runtime_renderer
415
+ case runtime
416
+ when "rails"
417
+ require_relative "rails/replay"
418
+ Upkeep::Rails::Replay
419
+ else
420
+ raise "replay recipe has no renderer"
421
+ end
422
+ end
423
+ end
424
+
425
+ def symbolize_keys(value)
426
+ case value
427
+ when Hash
428
+ value.each_with_object({}) do |(key, nested_value), result|
429
+ normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
430
+ result[normalized_key] = symbolize_keys(nested_value)
431
+ end
432
+ when Array
433
+ value.map { |nested_value| symbolize_keys(nested_value) }
434
+ else
435
+ value
436
+ end
437
+ end
438
+ end
439
+ end