fix: coordinate batch refreshes by revision - #13
Conversation
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 SummaryThe 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.
Confidence Score: 5/5The 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
Reviews (2): Last reviewed commit: "fix: track every concurrent same-revisio..." | Re-trigger Greptile |
| const key = `${group}:${revision}` | ||
| const controller = new AbortController() | ||
| activeBatches.set(group, controller) | ||
| activeBatches.set(key, { controller, group, revision: parsed }) |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
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.
|
@greptileai review |
Fixes the browser-side race you reported. Your diagnosis was exactly right.
The bug
activeBatcheswas keyed byscope:batchonly, and every new request aborted the previous onefor 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_controlsoften did not update.Why the existing tests missed it
Two reasons, both mine, and both now fixed:
fetchignored the abort signal. Aborted requests resolved anyway, so no testcould observe cancellation. This is the direct reason a suite of 23 passing tests shipped a
cancellation bug.
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, andsupersedeOlderRequestscancels only entrieswhose 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-revisiononly advances once Turbo applies the stream, so concurrentsame-revision responses would otherwise morph the same target twice.
Behaviour
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 take0.7.3.
Validation
Standard Ruby, RuboCop, RBS, Steep, and Brakeman clean. Version 0.7.3.