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.
- 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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 454ec3e93fcb61e9fb7a289317921cac5f48888f3e6c93dd22907aea9a2b3195
|
|
4
|
+
data.tar.gz: 0e63a35b4a3c7711b93278b651eca080e24f8fea04ab76c0be08f3dfab3295f8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dbf874698197ec44dadf7c0fb9ad70f5027828c4ec159793da16e33b5a16cc6e8af7a5a6a7d73e474f2e1f80103d5180edc2561bf6cba0e8c2e537e364834ae0
|
|
7
|
+
data.tar.gz: 40a66ba3ddb9861fbe3aded55f33f3fd5b87baf422935c44a4fa03297e09fccf95aae706a679a935e7672506184de10d1ad548a1610865d1acc930d89e37c965
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felipe Anjos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# Upkeep Rails
|
|
2
|
+
|
|
3
|
+
Upkeep Rails refreshes ordinary Rails pages when the data, request inputs, or
|
|
4
|
+
identity values they used change.
|
|
5
|
+
|
|
6
|
+
A successful HTML GET captures what the page rendered. A later Active Record
|
|
7
|
+
commit emits facts about what changed. Upkeep matches those facts to affected
|
|
8
|
+
rendered frames and delivers Turbo Stream updates over ActionCable.
|
|
9
|
+
|
|
10
|
+
The design goal is Rails-shaped DX: controllers load state, views render ERB,
|
|
11
|
+
models commit writes, and Upkeep derives the reactive boundary from the Rails
|
|
12
|
+
surfaces it observes. There is no query catalog, no `watch` or `track` DSL, and
|
|
13
|
+
no host-maintained list of identity dimensions.
|
|
14
|
+
|
|
15
|
+
## Why Upkeep
|
|
16
|
+
|
|
17
|
+
In ordinary Rails and Turbo code, the write can stay in the controller and the
|
|
18
|
+
stream response can live in a template. The flow still has to name every stream
|
|
19
|
+
target, counter, partial, or page region that might now be stale:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# app/controllers/cards_controller.rb
|
|
23
|
+
class CardsController < ApplicationController
|
|
24
|
+
def update
|
|
25
|
+
@card = Card.find(params[:id])
|
|
26
|
+
@card.update!(card_params)
|
|
27
|
+
|
|
28
|
+
@board = @card.board
|
|
29
|
+
@open_card_count = @board.cards.open.count
|
|
30
|
+
|
|
31
|
+
respond_to do |format|
|
|
32
|
+
format.turbo_stream
|
|
33
|
+
format.html { redirect_to @board }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```erb
|
|
40
|
+
<%# app/views/cards/update.turbo_stream.erb %>
|
|
41
|
+
<%= turbo_stream.replace dom_id(@card),
|
|
42
|
+
partial: "cards/card",
|
|
43
|
+
locals: { card: @card } %>
|
|
44
|
+
|
|
45
|
+
<%= turbo_stream.update "open_card_count", @open_card_count %>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
That works, but the update flow is coupled to the UI it happens to refresh.
|
|
49
|
+
Adding another dependent page, sidebar, filter, or counter means revisiting old
|
|
50
|
+
stream templates, controller assignments, callbacks, or broadcasts.
|
|
51
|
+
|
|
52
|
+
With Upkeep, the controller performs the domain action and stops:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
class CardsController < ApplicationController
|
|
56
|
+
def update
|
|
57
|
+
Card.find(params[:id]).update!(card_params)
|
|
58
|
+
head :ok
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The GET that rendered the page already recorded which templates, collections,
|
|
64
|
+
records, request values, and identity values the response used. When the commit
|
|
65
|
+
lands, Upkeep selects the affected frames, rerenders the narrowest proven
|
|
66
|
+
target, and leaves unrelated subscribers alone.
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
Upkeep Rails is early alpha. Application code should start from the documented
|
|
71
|
+
public entry points:
|
|
72
|
+
|
|
73
|
+
- `gem "upkeep-rails"` - load the Railtie.
|
|
74
|
+
- `bin/rails generate upkeep:install` - install storage, cable, and browser
|
|
75
|
+
bootstrap files.
|
|
76
|
+
- `config.upkeep.enabled` - enable or disable runtime capture.
|
|
77
|
+
- `config.upkeep.subscription_store` - choose `:active_record` or explicit
|
|
78
|
+
development/test `:memory` storage.
|
|
79
|
+
- `config.upkeep.refused_boundary_behavior` - raise or warn when a reactive
|
|
80
|
+
boundary cannot be proven.
|
|
81
|
+
- `Upkeep::Rails::Cable::Channel` - the generated browser client subscribes to
|
|
82
|
+
this channel.
|
|
83
|
+
- `Upkeep::Rails::Testing` - integration test helpers.
|
|
84
|
+
|
|
85
|
+
Everything under `Upkeep::Runtime`, `Upkeep::Dependencies`,
|
|
86
|
+
`Upkeep::Invalidation`, `Upkeep::Subscriptions`, `Upkeep::Delivery`,
|
|
87
|
+
`Upkeep::DAG`, probes, proofs, and benchmark harness code is internal.
|
|
88
|
+
|
|
89
|
+
## Core Concepts
|
|
90
|
+
|
|
91
|
+
### Rendered Page
|
|
92
|
+
|
|
93
|
+
A **rendered page** is a successful HTML GET that Upkeep can keep fresh. The
|
|
94
|
+
request runs normally through Rails. Upkeep observes the controller, Action View
|
|
95
|
+
rendering, Active Record reads, request inputs, and identity inputs used by the
|
|
96
|
+
response.
|
|
97
|
+
|
|
98
|
+
A rendered page describes *what the browser saw*.
|
|
99
|
+
|
|
100
|
+
### Frame
|
|
101
|
+
|
|
102
|
+
A **frame** is a rendered page, template, partial, collection render site, or
|
|
103
|
+
fragment with a stable delivery target. Frames let Upkeep refresh a specific
|
|
104
|
+
part of the page instead of replaying the whole response when a narrower update
|
|
105
|
+
is proven safe.
|
|
106
|
+
|
|
107
|
+
A frame describes *where a refresh can land*.
|
|
108
|
+
|
|
109
|
+
### Surface
|
|
110
|
+
|
|
111
|
+
A **surface** is the set of facts about future writes that would make a frame
|
|
112
|
+
stale. For Active Record, Upkeep derives surfaces from observed record
|
|
113
|
+
attributes, rendered collections, and relation shape where Rails exposes a
|
|
114
|
+
structural Arel query.
|
|
115
|
+
|
|
116
|
+
For example, a rendered collection of open cards ordered by position produces a
|
|
117
|
+
surface tied to the cards table, the columns that decide membership, and the
|
|
118
|
+
records rendered in that collection. A card update only selects the frames whose
|
|
119
|
+
surface it can affect.
|
|
120
|
+
|
|
121
|
+
A surface describes *when to rerender*.
|
|
122
|
+
|
|
123
|
+
### Identity Boundary
|
|
124
|
+
|
|
125
|
+
An **identity boundary** is observed viewer-specific state: `CurrentAttributes`,
|
|
126
|
+
Warden or Devise users, session values, cookies, request values, and ActionCable
|
|
127
|
+
connection identifiers. Upkeep uses these observed values to decide whether
|
|
128
|
+
work can be shared or must stay partitioned by viewer.
|
|
129
|
+
|
|
130
|
+
An identity boundary describes *who may share a result*.
|
|
131
|
+
|
|
132
|
+
### Subscription
|
|
133
|
+
|
|
134
|
+
A **subscription** is the browser's live connection back to the captured page.
|
|
135
|
+
Upkeep injects a marker into successful HTML responses. The generated browser
|
|
136
|
+
bootstrap reads that marker, subscribes over ActionCable, and applies received
|
|
137
|
+
Turbo Stream payloads.
|
|
138
|
+
|
|
139
|
+
A subscription describes *where updates should be sent*.
|
|
140
|
+
|
|
141
|
+
### Proven Delivery
|
|
142
|
+
|
|
143
|
+
**Proven delivery** means Upkeep only emits the narrowest update it can justify.
|
|
144
|
+
It may `append`, `prepend`, `remove`, `replace`, or replay an enclosing render
|
|
145
|
+
site depending on the proof available. If Upkeep cannot prove a boundary, it
|
|
146
|
+
refuses registration instead of silently widening into unsafe broad
|
|
147
|
+
invalidation.
|
|
148
|
+
|
|
149
|
+
Proven delivery describes *how much to refresh safely*.
|
|
150
|
+
|
|
151
|
+
### Glossary
|
|
152
|
+
|
|
153
|
+
| Term | What it means |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| Capture | The GET-time observation of render structure, data reads, request inputs, and identity inputs. |
|
|
156
|
+
| Commit facts | Active Record lifecycle facts such as table, model, id, changed attributes, and old/new values. |
|
|
157
|
+
| Replay | Rerendering a captured page, render site, or fragment with the observed inputs needed for that target. |
|
|
158
|
+
| Shared render | One replay reused for multiple anonymous or public subscribers with the same safe delivery shape. |
|
|
159
|
+
| Opaque | Visible to application code, but not structurally inspectable enough for Upkeep to prove dependencies, replay inputs, or delivery targets. Raw SQL strings, process-local objects, and ambiguous HTML roots are common examples. |
|
|
160
|
+
| Refused boundary | A page Upkeep did not register because a query, identity input, replay input, or DOM target could not be proven. |
|
|
161
|
+
|
|
162
|
+
## Quick Start
|
|
163
|
+
|
|
164
|
+
### 1. Add Upkeep
|
|
165
|
+
|
|
166
|
+
Add the gem to a Rails app:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
gem "upkeep-rails"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The Railtie installs hooks when Rails loads Active Record, Action Controller,
|
|
173
|
+
and Action View. The runtime is enabled by default.
|
|
174
|
+
|
|
175
|
+
### 2. Run The Installer
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
bin/rails generate upkeep:install
|
|
179
|
+
bin/rails db:migrate
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The generator creates subscription tables, writes `config/initializers/upkeep.rb`,
|
|
183
|
+
mounts ActionCable when needed, pins Turbo and ActionCable for importmap apps,
|
|
184
|
+
and imports the browser bootstrap from `app/javascript/application.js`.
|
|
185
|
+
|
|
186
|
+
### 3. Configure Subscription Storage
|
|
187
|
+
|
|
188
|
+
Production uses the ActiveRecord subscription store by default and fails fast
|
|
189
|
+
when the Upkeep subscription tables are missing:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
Rails.application.configure do
|
|
193
|
+
config.upkeep.enabled = true
|
|
194
|
+
config.upkeep.subscription_store = :active_record
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For development or isolated tests that do not run the installer migration, opt
|
|
199
|
+
into the in-process store explicitly:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
config.upkeep.subscription_store = :memory
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
For more than one Puma worker, configure ActionCable with a shared adapter such
|
|
206
|
+
as Redis or Solid Cable. The subscription store decides which subscribers need
|
|
207
|
+
work; ActionCable decides which worker owns each WebSocket connection.
|
|
208
|
+
|
|
209
|
+
### 4. Render Normal Rails Views
|
|
210
|
+
|
|
211
|
+
Controllers keep loading Active Record models and relations:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class BoardsController < ApplicationController
|
|
215
|
+
def show
|
|
216
|
+
@board = Board.find(params[:id])
|
|
217
|
+
@cards = @board.cards.order(:position)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Templates keep rendering ERB and partial collections:
|
|
223
|
+
|
|
224
|
+
```erb
|
|
225
|
+
<main>
|
|
226
|
+
<h1><%= @board.name %></h1>
|
|
227
|
+
|
|
228
|
+
<ul id="cards">
|
|
229
|
+
<%= render partial: "cards/card", collection: @cards, as: :card %>
|
|
230
|
+
</ul>
|
|
231
|
+
</main>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Successful HTML GET responses are captured automatically. Upkeep records the
|
|
235
|
+
rendered page, render sites, fragments, collection surfaces, identity inputs,
|
|
236
|
+
and request inputs, then injects the subscription marker into the response.
|
|
237
|
+
|
|
238
|
+
### 5. Keep Write Paths Focused
|
|
239
|
+
|
|
240
|
+
Writes keep doing domain work:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
class CardsController < ApplicationController
|
|
244
|
+
def update
|
|
245
|
+
Card.find(params[:id]).update!(card_params)
|
|
246
|
+
head :ok
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
After the commit, Upkeep dispatches invalidation facts, selects matching
|
|
252
|
+
subscriptions, rerenders the narrowest proven target, and sends Turbo Stream
|
|
253
|
+
payloads to the connected browsers.
|
|
254
|
+
|
|
255
|
+
## How Refresh Works
|
|
256
|
+
|
|
257
|
+
1. A successful HTML GET captures a rendered page.
|
|
258
|
+
2. The browser subscribes using the injected `data-upkeep-subscription` marker.
|
|
259
|
+
3. Active Record commits emit lifecycle facts.
|
|
260
|
+
4. Upkeep matches those facts against active surfaces.
|
|
261
|
+
5. Matching frames replay or use a proven stream operation.
|
|
262
|
+
6. Equivalent public targets render once and fan out to matching subscribers.
|
|
263
|
+
7. Identity-bound targets remain partitioned by observed identity boundaries.
|
|
264
|
+
|
|
265
|
+
## What Upkeep Observes
|
|
266
|
+
|
|
267
|
+
Render structure:
|
|
268
|
+
|
|
269
|
+
- Rails-resolved page templates.
|
|
270
|
+
- Partial and object partial renders.
|
|
271
|
+
- Collection render sites and their child fragments.
|
|
272
|
+
- Single-root fragment and render-site DOM targets.
|
|
273
|
+
|
|
274
|
+
Data dependencies:
|
|
275
|
+
|
|
276
|
+
- Active Record attribute reads.
|
|
277
|
+
- Active Record relation collection renders.
|
|
278
|
+
- Active Record callback writes and bulk `update_all` / `delete_all` writes.
|
|
279
|
+
- Relation table/column coverage derived from Arel where Rails exposes a
|
|
280
|
+
structural query shape.
|
|
281
|
+
|
|
282
|
+
Identity and ambient inputs:
|
|
283
|
+
|
|
284
|
+
- `ActiveSupport::CurrentAttributes` reads.
|
|
285
|
+
- Warden and Devise user reads through Warden.
|
|
286
|
+
- Session and cookie reads.
|
|
287
|
+
- Request values such as host, path, params, user agent, and remote IP.
|
|
288
|
+
- ActionCable connection identifiers.
|
|
289
|
+
|
|
290
|
+
## What Upkeep Cannot Capture
|
|
291
|
+
|
|
292
|
+
Upkeep captures reactive facts, not arbitrary Ruby execution. A boundary is
|
|
293
|
+
capturable only when Upkeep can prove the future write facts that affect it, the
|
|
294
|
+
target that can be replayed or patched, and the identity inputs that decide
|
|
295
|
+
whether it can be shared.
|
|
296
|
+
|
|
297
|
+
`Opaque` means Upkeep can see that application code used something, but Rails
|
|
298
|
+
did not expose enough structure for Upkeep to decide what future change should
|
|
299
|
+
refresh it or how to replay it safely.
|
|
300
|
+
|
|
301
|
+
This is a safety rule, not a parser preference. A live boundary must answer
|
|
302
|
+
three questions:
|
|
303
|
+
|
|
304
|
+
1. Which future write facts can make this rendered result stale?
|
|
305
|
+
2. Which target can Upkeep replay or patch when that happens?
|
|
306
|
+
3. Which observed identity inputs decide whether the result can be shared?
|
|
307
|
+
|
|
308
|
+
If any answer is missing, Upkeep would have to choose between missing updates,
|
|
309
|
+
refreshing too broadly, or sharing viewer-specific output with the wrong
|
|
310
|
+
subscriber. It refuses that boundary instead.
|
|
311
|
+
|
|
312
|
+
| Surface | Why it is not capturable | Developer experience |
|
|
313
|
+
| --- | --- | --- |
|
|
314
|
+
| Opaque Active Record relations: raw SQL predicates, raw joins, raw `from` sources, unknown table aliases, opaque order expressions, or opaque pluck columns. | Rails no longer exposes enough structure to prove table, column, predicate, and lifecycle coverage. | Development/test raises `Upkeep::ActiveRecordQuery::OpaqueRelationError` before registering. Warn mode logs, emits `refused_boundary.upkeep`, and refuses the live boundary instead of widening to an unsafe dependency. Rewrite with structural Active Record or Arel when the boundary should be live. |
|
|
315
|
+
| Controller queries that are never rendered as a collection boundary. | There is no DOM collection surface where membership can be appended, removed, prepended, or replaced. | The page can still render normally. Scalar relation output can be tracked as a page-level dependency, but it does not unlock collection stream planning. Render the relation through a collection partial when collection lifecycle matters. |
|
|
316
|
+
| Reads from external stores or process state: Redis, HTTP APIs, files, global variables, class variables, singleton caches, background thread state, or service memoization. | Active Record commit facts cannot select these reads, and Upkeep has no source adapter for their lifecycle. | They are not live dependencies today. If another observed dependency causes a replay, normal Rails code may read the new value during that replay; the external read itself will not trigger one. Use existing app mechanisms, explicit broadcasts, or future source adapters for those domains. |
|
|
317
|
+
| Writes outside observed Active Record paths: direct connection SQL, writes in another datastore, or side effects that do not emit Upkeep change facts. | Upkeep cannot match a future change to an existing surface without a write fact. | No refresh is scheduled from that write. Prefer Active Record write APIs for capturable models, or keep a manual invalidation/broadcast path for sources Upkeep does not observe yet. |
|
|
318
|
+
| Replay inputs that cannot be rebuilt: arbitrary objects, procs, IO handles, open clients, or values that only exist in one Ruby process. | A captured target must be replayable later, often in a different request context. | Keep frame locals and render options to records, relations, arrays, hashes, literals, and observed request/session/cookie values. Non-replayable values block the narrow replay path until they are represented as stable data. |
|
|
319
|
+
| Patch targets Upkeep cannot identify in rendered HTML. | Delivery needs a stable page, render-site, fragment, or member target. Ambiguous or missing roots cannot be patched safely. | Upkeep uses the narrowest proven target. If a narrow target is not proven but an enclosing target is, delivery deoptimizes to the enclosing target; if no safe target exists, the boundary is refused or tests expose the missing target. |
|
|
320
|
+
|
|
321
|
+
## Query Shapes
|
|
322
|
+
|
|
323
|
+
Collection dependencies are accepted only with proven column coverage. Opaque
|
|
324
|
+
predicates or table-only sources are refused instead of widening into broad
|
|
325
|
+
invalidation.
|
|
326
|
+
|
|
327
|
+
Controller materialization is supported when the rendered value keeps a
|
|
328
|
+
structural relation proof:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
def index
|
|
332
|
+
@cards = Card.where(status: "open").order(:position).to_a
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
```erb
|
|
337
|
+
<%= render partial: "cards/card", collection: @cards, as: :card %>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Upkeep attaches the collection dependency to the rendered collection boundary,
|
|
341
|
+
not to every controller query. A materialized relation that is never rendered as
|
|
342
|
+
a collection is not a lifecycle dependency by itself.
|
|
343
|
+
|
|
344
|
+
Scalar relation output is tracked as a page-level query dependency:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
@tag_names = Tag.where(active: true).pluck(:name)
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Simple plucked columns are live and can select a page replay when they change.
|
|
351
|
+
They are not collection dependencies, so they do not participate in
|
|
352
|
+
append/remove/prepend planning.
|
|
353
|
+
|
|
354
|
+
## Identity And Sharing
|
|
355
|
+
|
|
356
|
+
Upkeep observes identity only when application code reads identity-shaped state.
|
|
357
|
+
If a page reads `Current.user`, session values, cookies, request values, or
|
|
358
|
+
connection identifiers, delivery is partitioned by those observed values.
|
|
359
|
+
|
|
360
|
+
If a page never reads identity state, it can stay anonymous-public. Anonymous
|
|
361
|
+
subscribers with the same subscription shape can share compiled subscription
|
|
362
|
+
structure and update renders. Upkeep does not share initial response HTML.
|
|
363
|
+
|
|
364
|
+
Session, cookie, and request replay stores only observed values needed to rerun
|
|
365
|
+
the page. Unread session keys, cookies, and request headers are not copied into
|
|
366
|
+
the replay payload.
|
|
367
|
+
|
|
368
|
+
## Refused Boundaries
|
|
369
|
+
|
|
370
|
+
Upkeep distinguishes a refused boundary from a deoptimization.
|
|
371
|
+
|
|
372
|
+
A **refused boundary** means Upkeep cannot prove correctness. In
|
|
373
|
+
development/test, refused boundaries raise by default. In production, they warn,
|
|
374
|
+
emit `refused_boundary.upkeep`, and skip live registration for that boundary by
|
|
375
|
+
default:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
config.upkeep.refused_boundary_behavior = :raise # or :warn
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
This is intentional. A page that cannot be proven should behave like ordinary
|
|
382
|
+
Rails HTML instead of registering a broad or unsafe live dependency.
|
|
383
|
+
|
|
384
|
+
A **deoptimization** means Upkeep can still prove correctness, but not the
|
|
385
|
+
cheapest operation. The page remains live, and delivery falls back to a broader
|
|
386
|
+
proven target such as a render site or page replay. Planning and delivery
|
|
387
|
+
telemetry record the deoptimization reason so benchmarks can separate safety
|
|
388
|
+
fallbacks from true refusal.
|
|
389
|
+
|
|
390
|
+
## Testing
|
|
391
|
+
|
|
392
|
+
Use `Upkeep::Rails::Testing` for app-level assertions around subscription
|
|
393
|
+
registration and delivery. See [Testing](docs/testing.md) for the local green
|
|
394
|
+
bar, benchmark apps, proof runner, and integration helpers.
|
|
395
|
+
|
|
396
|
+
Run the project test suite with the project Ruby:
|
|
397
|
+
|
|
398
|
+
```sh
|
|
399
|
+
mise exec -- ruby -S rake test
|
|
400
|
+
mise exec -- ruby bin/test
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`bin/test` runs the gem tests, both maintained benchmark app test suites, and
|
|
404
|
+
the proof runner. The proof runner writes JSON reports to `results/`.
|
|
405
|
+
|
|
406
|
+
## Further Reading
|
|
407
|
+
|
|
408
|
+
- [Getting Started](docs/guides/getting-started.md): add the package to a Rails
|
|
409
|
+
app, create subscription storage, configure ActionCable, and verify
|
|
410
|
+
subscription registration.
|
|
411
|
+
- [Identity And Sharing](docs/architecture/identity-and-sharing.md): how
|
|
412
|
+
CurrentAttributes, Warden/Devise, session, cookies, request values, and
|
|
413
|
+
ActionCable identifiers partition delivery.
|
|
414
|
+
- [Ambient Inputs Roadmap](docs/architecture/ambient-inputs-roadmap.md): how
|
|
415
|
+
controller, session, cookie, request, and before-action state should support
|
|
416
|
+
real Rails apps without whole-session replay or app-declared identity lists.
|
|
417
|
+
- [Query Dependencies](docs/architecture/query-dependencies.md): how Active
|
|
418
|
+
Record/Arel relation shape drives collection invalidation without a host
|
|
419
|
+
query catalog.
|
|
420
|
+
- [Production Roadmap](docs/production_roadmap.md): hardening work, benchmark
|
|
421
|
+
coverage, and compatibility tracking beyond the current release contract.
|
|
422
|
+
- [Cost Model Roadmap](docs/cost-model-roadmap.md): runtime cost boundaries,
|
|
423
|
+
Turbo baseline positioning, Herb-backed template structure, and the
|
|
424
|
+
large-scale direction for proven incremental rendering.
|