fix(web): stop Sidebar re-render loop from cache-event subscriber
setNow(Date.now()) was called on every query-cache event, including events dispatched synchronously during ServerWorkspaceRows' render. On CI, consecutive Date.now() calls cross millisecond boundaries so each call returns a new value — React always re-renders Sidebar, which re-renders the layout, which re-renders ServerWorkspaceRows, which fires more cache events. After ~25 cycles React throws "Maximum update depth exceeded." Fix: remove setNow from the cache subscriber. setNow now fires only from the 30s interval timer, where it correctly refreshes the "X ago" display text without triggering a render loop. Also adds a deterministic CI-repro test that mocks Date.now to return incrementing values and asserts the filter-change path completes without looping.
This commit is contained in:
@@ -60,7 +60,9 @@ function useLastDataUpdate(): string {
|
||||
|
||||
useEffect(() => {
|
||||
function refresh() {
|
||||
setNow(Date.now());
|
||||
// No setNow here — calling setNow on every cache event causes a render loop on
|
||||
// CI (each Date.now() call crosses a ms boundary → new value → React re-renders
|
||||
// Sidebar → cache events fire again → loop). setNow belongs only in the interval.
|
||||
const latest = queryClient
|
||||
.getQueryCache()
|
||||
.getAll()
|
||||
@@ -70,7 +72,10 @@ function useLastDataUpdate(): string {
|
||||
|
||||
refresh();
|
||||
const unsubscribe = queryClient.getQueryCache().subscribe(refresh);
|
||||
const interval = window.setInterval(refresh, 30_000);
|
||||
const interval = window.setInterval(() => {
|
||||
setNow(Date.now()); // refresh relative-time display ("X ago") every 30s
|
||||
refresh();
|
||||
}, 30_000);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
window.clearInterval(interval);
|
||||
|
||||
@@ -59,6 +59,22 @@ function renderDashboard() {
|
||||
describe("Dashboard — unified server-aware view", () => {
|
||||
afterEach(() => localStorage.clear());
|
||||
|
||||
it("does not loop when Date.now advances on each call (CI render-loop repro)", async () => {
|
||||
// On CI, consecutive Date.now() calls cross millisecond boundaries, so setNow(Date.now())
|
||||
// in the cache-event subscriber always produces a new value → React keeps re-rendering
|
||||
// Sidebar → hits the 25-cycle "Maximum update depth exceeded" limit.
|
||||
// This test forces that CI condition locally to catch regressions.
|
||||
let t = 1_000_000;
|
||||
const spy = vi.spyOn(Date, "now").mockImplementation(() => t++);
|
||||
saveStore({ instances: [neo, iris], activeId: "neo" });
|
||||
renderDashboard();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("(Neo)")).toBeInTheDocument();
|
||||
expect(screen.getByText("(Iris)")).toBeInTheDocument();
|
||||
});
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("lists each server's workspaces labelled with the server name", async () => {
|
||||
saveStore({ instances: [neo, iris], activeId: "neo" });
|
||||
renderDashboard();
|
||||
|
||||
Reference in New Issue
Block a user