Skip to content

feat: batch component refreshes and test the browser modules - #9

Merged
cardmagic merged 3 commits into
mainfrom
agent/batched-refreshes-and-js-tests
Aug 9, 2026
Merged

feat: batch component refreshes and test the browser modules#9
cardmagic merged 3 commits into
mainfrom
agent/batched-refreshes-and-js-tests

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

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 many
components in the group changed.

<%= actor.component :player,          key: 1, observes: :player_one,          batch: :playmat, refresh: :morph %>
<%= actor.component :player_controls, key: 1, observes: :player_one_controls, batch: :playmat, refresh: :morph %>
<%= actor.component :library_search,  key: 1, observes: :library,             batch: :playmat, refresh: :morph %>

Before, one mutation touching all three observables:

commit -> 3 invalidations -> 3 refresh elements -> 3 GET /solid_objects/components

After:

commit -> 3 invalidations -> 1 GET /solid_objects/components/batch -> 3 frames

That is acceptance criterion 1, and the batching half of criterion 6.

Frames inside a JSON envelope, and why

GET /solid_objects/components/batch takes the signed tokens[] plus the instance_id and
revision the 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 ComponentRenderer and Turbo untouched while giving the
client 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_query boundary an individual refresh uses. The batch
name 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 may
mix 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 javascript CI job that the release job now
depends 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

  • No database change, no migration, no initializer regeneration.
  • Signed token format: batch is a new optional key. Existing tokens verify unchanged and
    are page-scoped, so no deploy coordination is needed.
  • Existing applications are untouched. No batch: means the previous one-request-per-
    component path, byte for byte. The batch JS ships only when a scope uses the option.

Validation

bundle exec rake   # 268 runs, 1048 assertions, 0 failures
npm test           # 21 pass, 0 fail

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.

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.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds batched reactive-component refreshes and browser-module coverage while closing the previously reported scope-isolation, CI pinning, and generated-signature gaps.

  • Coalesces same-scope, same-revision component invalidations into one authorized batch request.
  • Adds revision-fenced frame application and isolation between actor scopes.
  • Adds Node/jsdom browser tests and makes them a release prerequisite.
  • Documents the batching protocol and updates the package version and generated RBS contracts.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
app/assets/javascripts/solid_objects/component_batch_refresh.js Coalesces batch notifications, isolates request state by actor scope, aborts superseded requests, and revision-fences frame application.
app/controllers/solid_objects/components_controller.rb Adds the bounded batch endpoint with single-actor/group validation, authorization through the existing renderer, and a documented JSON response.
lib/solid_objects/component_registration.rb Carries signed batch metadata through registration and exposes batch-refresh URL generation with updated RBS annotations.
lib/solid_objects/turbo_stream_renderer.rb Emits batch refresh notifications for changed registrations while preserving the individual refresh path.
.github/workflows/ci.yml Adds lockfile-based JavaScript testing with immutable action references and gates releases on that job.
test/javascript/component_batch_refresh.test.mjs Covers coalescing, frame application, errors, stale revisions, and regression cases for cross-scope isolation.

Sequence Diagram

sequenceDiagram
  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
Loading

Reviews (2): Last reviewed commit: "fix: isolate batch requests per actor sc..." | Re-trigger Greptile

Comment thread app/assets/javascripts/solid_objects/component_batch_refresh.js Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment on lines +22 to +23
- uses: actions/checkout@v7
- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security 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.

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!

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai review

@cardmagic
cardmagic merged commit 8df1773 into main Aug 9, 2026
13 checks passed
@cardmagic
cardmagic deleted the agent/batched-refreshes-and-js-tests branch August 10, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant