feat: batch component refreshes and test the browser modules - #9
Conversation
Components sharing a batch collapse into one browser request per revision instead of one per component. A new endpoint returns HTML frames inside a JSON envelope, so ERB rendering and Turbo morph are unchanged while the client gets a documented contract with per-frame revisions. Duplicate notifications for one batch and revision coalesce in the browser, unchanged components are never requested, and stale frames cannot overwrite newer targets. The batch name is signed into the component token. Adds a Node test-runner and jsdom suite covering both browser modules, wired into CI.
npm install left node_modules staged, which put 1232 dependency files in the branch and pushed it past the review file limit. Ignore the directory and keep package-lock.json so npm ci still installs a pinned tree. Version the release 0.7.0 rather than 0.6.1: batch:, the batch endpoint, and the batch token key are new public API.
|
@greptileai review |
Greptile SummaryThe PR adds batched reactive-component refreshes and browser-module coverage while closing the previously reported scope-isolation, CI pinning, and generated-signature gaps.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Sequence DiagramsequenceDiagram
participant Cable as Action Cable
participant Browser as Batch refresh element
participant API as Batch endpoint
participant Renderer as Component renderer
Cable->>Browser: Component invalidations
Browser->>Browser: Group by scope, batch, and revision
Browser->>API: GET components/batch with signed tokens
loop Changed component
API->>Renderer: Authorize and render
Renderer-->>API: Turbo frame HTML
end
API-->>Browser: JSON envelope containing frames
Browser->>Browser: Reject stale frames and apply current frames
Reviews (2): Last reviewed commit: "fix: isolate batch requests per actor sc..." | Re-trigger Greptile |
| - uses: actions/checkout@v7 | ||
| - uses: actions/setup-node@v4 |
There was a problem hiding this comment.
The new JavaScript job executes checkout@v7 and setup-node@v4 through mutable tags and is now required by the release job, so a retargeted tag can run untrusted code in CI or block every release. Pin both actions to immutable commit SHAs.
How this was verified: The workflow executes both mutable references before the test commands and lists the JavaScript job in the release job's needs dependencies.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci.yml
Line: 22-23
Comment:
**Mutable actions gate releases**
The new JavaScript job executes `checkout@v7` and `setup-node@v4` through mutable tags and is now required by the release job, so a retargeted tag can run untrusted code in CI or block every release. Pin both actions to immutable commit SHAs.
**How this was verified:** The workflow executes both mutable references before the test commands and lists the JavaScript job in the release job's `needs` dependencies.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fair, and it matters more here than it would have before, because the release job now depends on this one. Pinned both actions to commit SHAs in 829caae, tag kept as a trailing comment.
Noting for the record that the pre-existing jobs still use mutable tags, so the repository is not fully pinned. I limited the change to the job this PR introduces rather than reworking the whole workflow inside a feature PR. Pinning the rest is worth a follow-up.
Two actor scopes reusing a batch name shared the module-level pending and active maps, so their same-revision requests merged into one call the server rejects for mixing actors, and overlapping requests aborted each other. Key both maps by scope and batch. Also expose the batch keyword in the generated RBS signatures, which the annotations had omitted, and pin the new JavaScript job's actions to commit SHAs now that the release job depends on it.
|
@greptileai review |
Closes the two gaps left open by #8: batched component refreshes, and a test suite for the
browser modules.
Batched component refreshes
Adding
batch:groups components so one revision costs one request no matter how manycomponents in the group changed.
Before, one mutation touching all three observables:
After:
That is acceptance criterion 1, and the batching half of criterion 6.
Frames inside a JSON envelope, and why
GET /solid_objects/components/batchtakes the signedtokens[]plus theinstance_idandrevisionthe browser holds, and returns:{ "actor_type": "playmat_room", "actor_id": "table-1", "batch": "playmat", "instance_id": 12, "revision": 48, "frames": [ { "target": "...", "revision": "12:48", "refresh_method": "morph", "html": "<turbo-frame ...>" } ] }HTML alone would force the client to pick frames out of an undocumented document.
Pure JSON state would mean a second renderer and would abandon ERB and Turbo morph.
Frames inside a JSON envelope keeps
ComponentRendererand Turbo untouched while giving theclient a documented contract with per-frame revisions. That is the recommendation from #8, now
implemented.
Where coalescing happens, and why not on the server
Broadcasts are one row per observable and arrive as separate Action Cable messages, so the
channel sees one invalidation at a time and cannot know which sibling observables are about to
change at the same revision. Suppressing later notifications server-side would drop components
that changed, and buffering in the channel would add latency to every update.
So the channel emits one batch element per invalidation carrying only the components that
actually changed, and the browser merges notifications for the same batch and revision in a
microtask into a single request. The number of HTTP requests, which is what the latency budget
cares about, goes to one. Action Cable messages remain one per changed observable.
Guarantees
Only components whose dependencies changed are named in a batch. Duplicate notifications for one
batch and revision merge; a superseded request for the same batch is aborted. Each frame carries
its own revision and cannot overwrite a target holding a newer one. Authorization is unchanged:
every component passes the same
authorize_queryboundary an individual refresh uses. The batchname is signed into the component token, so a browser cannot invent or widen a group, and a batch
mixing actors or groups is rejected. Components without
batch:are untouched, and a scope maymix batched and unbatched freely.
JavaScript test suite
The browser modules were previously unverified by CI. This adds Node's built-in test runner plus
jsdom, no test framework dependency, wired in as a
javascriptCI job that the release job nowdepends on.
21 tests across both modules: batch coalescing, per-token deduplication, batch and revision
isolation, frame application, stale-frame rejection, absent targets, error reporting, and for
payloads the identity/revision contract, stale and duplicate suppression, per-name tracking, and
malformed input.
The suite immediately earned its keep by catching two real bugs in the tests I had just written
(shared scope identity across tests, and applied frames leaking between cases), both of which
would have masked regressions.
Compatibility
batchis a new optional key. Existing tokens verify unchanged andare page-scoped, so no deploy coordination is needed.
batch:means the previous one-request-per-component path, byte for byte. The batch JS ships only when a scope uses the option.
Validation
Standard Ruby, RuboCop, RBS, Steep, and Brakeman clean. 11 new Ruby integration tests cover the
grouping, changed-only selection, stale and repeated revisions, separate batches, unbatched
behaviour, mixed scopes, token signing, forged batch names, and the batch URL.
Still open
End-to-end benchmark evidence (request counts and wall-clock latency against a running app) is
not here. The request-count claim is verified by tests; the latency claim is not yet measured.