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.

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