Skip to content

fix: coordinate batch refreshes by revision - #13

Merged
cardmagic merged 2 commits into
mainfrom
agent/fix-batch-revision-races
Aug 9, 2026
Merged

fix: coordinate batch refreshes by revision#13
cardmagic merged 2 commits into
mainfrom
agent/fix-batch-revision-races

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Fixes the browser-side race you reported. Your diagnosis was exactly right.

The bug

activeBatches was keyed by scope:batch only, and every new request aborted the previous one
for that group, including requests for the same revision. The microtask merge only coalesces
notifications that arrive in one task; invalidations for one revision arrive as separate WebSocket
messages, so each one issued its own request and cancelled the one before it. Only the last
response survived, which is why player_controls often did not update.

Why the existing tests missed it

Two reasons, both mine, and both now fixed:

  1. The mock fetch ignored the abort signal. Aborted requests resolved anyway, so no test
    could observe cancellation. This is the direct reason a suite of 23 passing tests shipped a
    cancellation bug.
  2. The "three components" test fired all three notifications in one task, so the microtask
    merge hid the problem. Production delivers them in separate tasks.

The new suite honours the signal and delivers notifications in separate tasks with responses
genuinely in flight. Verified against the shipped 0.7.2 module: the key test fails there and
passes here.

The fix

Requests are tracked per scope:batch:revision, and supersedeOlderRequests cancels only entries
whose revision is strictly older than the incoming one. Same-revision requests run alongside each
other and every frame is applied.

Frames already applied at a revision are tracked separately, because a target's own
data-solid-objects-revision only advances once Turbo applies the stream, so concurrent
same-revision responses would otherwise morph the same target twice.

Behaviour

Case Result
Same scope, batch, revision No cancellation, all frames applied, tokens deduplicated
Same scope and batch, newer revision Older in-flight request superseded
Different scope or batch name Independent

Preserved unchanged: signed-token validation, stale revision fencing, target validation,
same-origin checks, HTTP error events, malformed-response events, Turbo morph behaviour, and
single-component refresh.

Tests

25 JavaScript tests, covering all eight cases you listed: three same-revision invalidations in
separate tasks, every frame eventually applied, no same-revision request aborting another, a newer
revision superseding an older in-flight request, older frames unable to overwrite newer targets,
scope isolation, duplicate tokens sent once, and both error events. Plus batch-name independence,
duplicate frame suppression, cross-origin refusal, and that a superseded request reports no error.

Compatibility

Browser module only. No database change, no migration, no signed-token change, no server change.
Anyone on 0.7.0 through 0.7.2 using batch: with more than one component per revision should take
0.7.3.

Validation

bundle exec rake   # 287 runs, 1103 assertions, 0 failures
npm test           # 25 pass, 0 fail

Standard Ruby, RuboCop, RBS, Steep, and Brakeman clean. Version 0.7.3.

Invalidations for one revision arrive as separate WebSocket messages, so the microtask merge could not see them all and each request aborted the one before it, leaving only the last component updated. Track requests per scope, batch, and revision, and cancel only when a strictly newer revision arrives. Same-revision requests now run alongside each other and every frame is applied, with frames already applied at a revision skipped. The test harness now honours the abort signal; without that no test could observe cancellation, which is why this shipped.
@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown

Greptile Summary

The PR coordinates batch refresh requests by revision and gives each concurrent request a unique tracking entry, ensuring a newer revision supersedes every older in-flight request.

  • Allows same-revision requests to complete independently.
  • Deduplicates frames already applied at a revision.
  • Adds abort-aware regression coverage for concurrent refreshes and supersession.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported controller-overwrite issue is fixed by uniquely tracking each request and superseding all older entries.

Important Files Changed

Filename Overview
app/assets/javascripts/solid_objects/component_batch_refresh.js The unique per-request keys fully address the previously reported controller overwrite and ensure all older requests are aborted without emitting error events.
test/javascript/component_batch_refresh.test.mjs The abort-aware harness and concurrent-request regression test directly cover the previously reported failure.
docs/realtime.md Documentation accurately describes same-revision concurrency and newer-revision supersession.

Reviews (2): Last reviewed commit: "fix: track every concurrent same-revisio..." | Re-trigger Greptile

Comment on lines +52 to +54
const key = `${group}:${revision}`
const controller = new AbortController()
activeBatches.set(group, controller)
activeBatches.set(key, { controller, group, revision: parsed })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Same-revision controllers overwrite each other

When multiple same-revision requests are in flight and a newer revision arrives, each request has written to the same ${group}:${revision} entry, so supersedeOlderRequests aborts only the last controller. Earlier requests continue running and can emit solid-objects:batch-refresh-error after the newer refresh has superseded them.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/assets/javascripts/solid_objects/component_batch_refresh.js
Line: 52-54

Comment:
**Same-revision controllers overwrite each other**

When multiple same-revision requests are in flight and a newer revision arrives, each request has written to the same `${group}:${revision}` entry, so `supersedeOlderRequests` aborts only the last controller. Earlier requests continue running and can emit `solid-objects:batch-refresh-error` after the newer refresh has superseded them.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

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.

Correct, and it partly defeated the fix. Same-revision requests run concurrently by design, but they all wrote to the same ${group}:${revision} entry, so activeBatches held only the last controller. A newer revision then superseded that one alone while the earlier requests kept running, wasting work and able to emit solid-objects:batch-refresh-error after being logically superseded.

Fixed in e4304e1: each request gets its own entry via a monotonic sequence, so supersedeOlderRequests reaches all of them. Correctness of applied DOM was never at risk, since applyFrame fences per target, but the cancellation and error behaviour was wrong.

Regression test: two concurrent same-revision requests in flight, then a newer revision, asserting both older requests are aborted and no error is reported. The harness now records aborted URLs. I verified the test fails without the fix and passes with it.

Same-revision requests run concurrently by design, but they shared one activeBatches key, so the map held only the last controller. A newer revision then superseded only that one and the earlier requests kept running, wasting work and able to report a refresh error after being logically superseded. Give each request its own entry so supersession reaches all of them.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai review

@cardmagic
cardmagic merged commit a6b8e08 into main Aug 9, 2026
13 checks passed
@cardmagic
cardmagic deleted the agent/fix-batch-revision-races branch August 9, 2026 17:36
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