fix: batch component refreshes on reconnect - #25
Conversation
Greptile SummaryThe PR makes reconnect refreshes use the same batching path as live invalidations and adds browser-level reconnect coverage.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Reconnect snapshot] --> B[Select stale registrations]
B --> C[refresh_streams]
C --> D{Registration has batch?}
D -->|No| E[Emit individual component refresh]
D -->|Yes| F[Group by batch]
F --> G[Record each registration revision]
G --> H[Emit one batch refresh per group]
Reviews (2): Last reviewed commit: "test: release every gated response on te..." | Re-trigger Greptile |
| await waitFor( | ||
| () => cancelled.has("before-drop"), | ||
| "the reconnect should cancel the request from before the drop" | ||
| ) | ||
| await page.waitForFunction( | ||
| () => document.getElementById("player").dataset.solidObjectsRevision === "1:12", | ||
| null, | ||
| { timeout: 5000 } | ||
| ) | ||
| stalled.release() |
There was a problem hiding this comment.
When a preceding wait or assertion fails, execution skips the sole stalled.release() call, leaving the route blocked on its pending response and allowing browser-suite teardown to hang until timeout.
| await waitFor( | |
| () => cancelled.has("before-drop"), | |
| "the reconnect should cancel the request from before the drop" | |
| ) | |
| await page.waitForFunction( | |
| () => document.getElementById("player").dataset.solidObjectsRevision === "1:12", | |
| null, | |
| { timeout: 5000 } | |
| ) | |
| stalled.release() | |
| try { | |
| await waitFor( | |
| () => cancelled.has("before-drop"), | |
| "the reconnect should cancel the request from before the drop" | |
| ) | |
| await page.waitForFunction( | |
| () => document.getElementById("player").dataset.solidObjectsRevision === "1:12", | |
| null, | |
| { timeout: 5000 } | |
| ) | |
| } finally { | |
| stalled.release() | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/browser/reconnect.test.mjs
Line: 218-227
Comment:
**Gate lacks failure cleanup**
When a preceding wait or assertion fails, execution skips the sole `stalled.release()` call, leaving the route blocked on its pending response and allowing browser-suite teardown to hang until timeout.
```suggestion
try {
await waitFor(
() => cancelled.has("before-drop"),
"the reconnect should cancel the request from before the drop"
)
await page.waitForFunction(
() => document.getElementById("player").dataset.solidObjectsRevision === "1:12",
null,
{ timeout: 5000 }
)
} finally {
stalled.release()
}
```
---
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.
Applied in 3a2c41b, but more generally than the suggestion. A try/finally fixes the one test that exists; the next gated test has to remember. gate() now registers its release and an afterEach hook drains them, so a test cannot reintroduce this by forgetting.
One correction to the finding: the hang does not reproduce. I forced the cancellation assertion to fail and removed the cleanup, and the suite reported the failure and exited in seven seconds rather than hanging. after closes the browser first, which destroys the client socket; the suspended route function then holds no live handle, so nothing keeps the runner alive. The cleanup is still worth having, and it is in.
|
@greptileai review |
Mapping the reconnect path for browser coverage turned up a defect. A reconnecting subscription refreshed every stale component individually and ignored the batches those components declared: refreshes_for partitioned by batch and grouped, reconnect_refreshes did not. A page with twenty batched components therefore issued twenty requests where a live invalidation issues one. That is the worst possible moment for request amplification. Reconnects cluster: a server restart or a network blip reconnects every client at once, so the amplification lands as a burst rather than spread over time. Both paths now share refresh_streams, so a reconnecting client pays what a connected one pays. The browser suite gains the reconnect burst it was missing: convergence of batched and unbatched components, an inert replay of an already-applied revision, cancellation of the request left in flight by the drop, incarnation ordering after a destroy and recreate, and payload delivery exactly once per revision. Each was verified to fail against a mutated module rather than only to pass against the current one.
A gate the test never releases, because an assertion failed before it got there, leaves its route awaiting forever. Gates now register themselves and an afterEach hook drains them, so this cannot be reintroduced by a future test that forgets. The reported hang does not reproduce: closing the browser destroys the client socket, so the suspended route holds no live handle and the runner exits. Forcing the assertion to fail reported the failure and exited in seven seconds. The cleanup is worth having regardless.
3a2c41b to
f393121
Compare
Adds the browser reconnect coverage the roadmap called for, and fixes a defect that writing it turned up.
The defect
ComponentSubscriptions#refreshes_forpartitions changed registrations bybatchand emits onesolid-objects-batch-refreshper batch.#reconnect_refreshesdid neither: it mapped every stale registration throughrefreshindividually, so a reconnecting client ignored the batches its own components declared.A page with twenty batched components issued twenty requests on reconnect where a live invalidation issues one.
The timing is what makes it matter. Reconnects cluster. A server restart or a network blip reconnects every client at once, so the amplification arrives as a burst at exactly the moment the app is least able to absorb it.
Both paths now share a private
refresh_streams, so a reconnecting client pays what a connected one pays.Coverage
Five Ruby tests in
component_batch_test.rbfor the reconnect path: batching, mixed batched/unbatched, distinct batches, revision recording for every batched registration, and no refresh for components already current. Three failed against the old code.test/browser/reconnect.test.mjscovers the reconnect burst in real Chromium against a real Turbo build:2:1must win over1:12Each was verified by mutating the module and confirming the test fails:
applyFramerevision guardssupersedeOlderRequestsreturns earlyThat last check is the point of the exercise. Every batching defect that reached production passed a test suite; passing is not evidence a test can fail.
Validation
docs/roadmap.mdmoves reconnect coverage out of the milestone, records that reconnect bypassed batching, and milestone 3 reduces to Turbo append intents.docs/realtime.mddocuments that reconnect refreshes a batch together.