Skip to content

fix: Stale canvas events shown when switching between canvases without page refresh#3506

Merged
forestileao merged 2 commits intomainfrom
cursor/cli-login-api-key-a13a
Mar 24, 2026
Merged

fix: Stale canvas events shown when switching between canvases without page refresh#3506
forestileao merged 2 commits intomainfrom
cursor/cli-login-api-key-a13a

Conversation

@cursor
Copy link
Copy Markdown
Contributor

@cursor cursor Bot commented Mar 17, 2026

Summary

Fixes #3504

When navigating between canvases without a page refresh, the UI displayed stale event data from the React Query cache instead of the latest state. WebSocket updates continued to work correctly, but the initial data rendered came from the cached canvas status response, which was outdated.

Root Cause

The issue had two contributing factors:

  1. Global staleTime of 5 minutes — The React Query client is configured with a global staleTime: 5 * 60 * 1000. This meant cached canvas detail data (which includes status with lastExecutions, nextQueueItems, lastEvents) was considered "fresh" and served without triggering a background refetch when navigating back to a previously visited canvas.

  2. Store initialization guard — The hasInitializedStoreRef in the workflow page prevented the node execution Zustand store from re-initializing after a background refetch completed with fresh data. Once the store was initialized from stale cache, it stayed stale.

Changes

web_src/src/hooks/useCanvasData.ts

  • Set staleTime: 0 on useCanvas and useCanvasEvents hooks to override the global 5-minute default. This ensures a background refetch is always triggered when the query key changes (i.e., when canvasId changes), so fresh data is fetched on every canvas switch.

web_src/src/pages/workflowv2/index.tsx

  • Immediate init + deferred re-init: When switching canvases, the store initializes immediately from cached data (so nodes render with their events right away — no loading gap). A pendingStoreReinitRef flag then allows exactly one re-initialization when the background refetch completes with fresh data.
  • Reset on canvas switch: In the canvasId change effect, hasInitializedStoreRef is reset and pendingStoreReinitRef is set to true, enabling the re-initialization cycle for the incoming canvas.

How It Works

  1. User navigates from Canvas B to Canvas A
  2. canvasId change effect resets hasInitializedStoreRef = null and sets pendingStoreReinitRef = true
  3. React Query serves cached Canvas A data with isFetching: true (background refetch triggered due to staleTime: 0)
  4. Store initialization effect: ref mismatch → initializes immediately from cached data (no loading gap); since canvasFetching is true, pendingStoreReinitRef stays true
  5. Background refetch completes: fresh Canvas A data arrives, canvasFetching becomes false
  6. Store initialization effect: pendingStoreReinitRef is true and not fetching → re-initializes from fresh data, clears the flag
  7. WebSocket events continue to merge correctly with the fresh baseline
Open in Web Open in Cursor 

When navigating between canvases without a page refresh, the UI
displayed stale event data from the React Query cache. The root cause
was twofold:

1. The global staleTime of 5 minutes meant cached canvas data was
   served without triggering a background refetch.
2. The hasInitializedStoreRef guard prevented the node execution store
   from re-initializing when a background refetch eventually completed
   with fresh data.

Changes:
- Set staleTime: 0 on useCanvas and useCanvasEvents hooks so cached
  data always triggers a background refetch when the query key changes.
- Guard store initialization with !canvasFetching to skip populating
  the store from stale cached data while a refetch is in progress.
- Reset hasInitializedStoreRef and clear the node execution store when
  canvasId changes, allowing the store to re-initialize from the fresh
  fetch result.

Closes #3504
@superplanehq-integration
Copy link
Copy Markdown

👋 Commands for maintainers:

  • /sp start - Start an ephemeral machine (takes ~30s)
  • /sp stop - Stop a running machine (auto-executed on pr close)

@forestileao
Copy link
Copy Markdown
Collaborator

/sp start

@superplanehq-integration
Copy link
Copy Markdown

✅ Ready.

Web: https://pr-3506-ephemeral.superplane.com
SSH: ssh -o StrictHostKeyChecking=no app@178.104.15.210
Logs: ssh -o StrictHostKeyChecking=no app@178.104.15.210 'cd superplane && make dev.logs.app'

@forestileao
Copy link
Copy Markdown
Collaborator

@cursoragent there is an UI thing that I don't like. The first node events are appearing after the nodes loaded. After the "Loading..."

@cursor
Copy link
Copy Markdown
Contributor Author

cursor Bot commented Mar 18, 2026

Revised the approach to eliminate the loading gap the reviewer noticed. The new strategy:

  • Before: Cleared the store and blocked initialization until the refetch completed, causing a visible "Loading..." flash where nodes appeared without their events.
  • After: The store initializes immediately from cached data (so nodes render with events right away), then re-initializes once when the background refetch completes with fresh data. A pendingStoreReinitRef flag controls this one-shot re-initialization cycle, ensuring WebSocket updates aren't disrupted by subsequent refetches.
Open in Web Open in Cursor 

@forestileao
Copy link
Copy Markdown
Collaborator

/sp stop

@superplanehq-integration
Copy link
Copy Markdown

✅ Ephemeral machine has been terminated.

Replace the blocking approach (clear store + skip init during refetch)
with an immediate-init + re-init pattern:

- Initialize the store immediately from cached data so nodes render
  with their events right away (no Loading... flash).
- Track a pendingStoreReinitRef flag that allows one re-initialization
  when the background refetch completes with fresh data.
- Remove clearNodeExecutionStore() from the canvas switch effect since
  initializeFromWorkflow already replaces all store data.
@forestileao forestileao merged commit 5fe6fbe into main Mar 24, 2026
4 checks passed
@forestileao forestileao deleted the cursor/cli-login-api-key-a13a branch March 24, 2026 03:41
EtnDiaz pushed a commit to EtnDiaz/superplane that referenced this pull request Mar 28, 2026
…t page refresh (superplanehq#3506)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Fixes superplanehq#3504

When navigating between canvases without a page refresh, the UI
displayed stale event data from the React Query cache instead of the
latest state. WebSocket updates continued to work correctly, but the
initial data rendered came from the cached canvas status response, which
was outdated.

## Root Cause

The issue had two contributing factors:

1. **Global `staleTime` of 5 minutes** — The React Query client is
configured with a global `staleTime: 5 * 60 * 1000`. This meant cached
canvas detail data (which includes `status` with `lastExecutions`,
`nextQueueItems`, `lastEvents`) was considered "fresh" and served
without triggering a background refetch when navigating back to a
previously visited canvas.

2. **Store initialization guard** — The `hasInitializedStoreRef` in the
workflow page prevented the node execution Zustand store from
re-initializing after a background refetch completed with fresh data.
Once the store was initialized from stale cache, it stayed stale.

## Changes

### `web_src/src/hooks/useCanvasData.ts`
- Set `staleTime: 0` on `useCanvas` and `useCanvasEvents` hooks to
override the global 5-minute default. This ensures a background refetch
is always triggered when the query key changes (i.e., when `canvasId`
changes), so fresh data is fetched on every canvas switch.

### `web_src/src/pages/workflowv2/index.tsx`
- **Immediate init + deferred re-init**: When switching canvases, the
store initializes immediately from cached data (so nodes render with
their events right away — no loading gap). A `pendingStoreReinitRef`
flag then allows exactly one re-initialization when the background
refetch completes with fresh data.
- **Reset on canvas switch**: In the `canvasId` change effect,
`hasInitializedStoreRef` is reset and `pendingStoreReinitRef` is set to
`true`, enabling the re-initialization cycle for the incoming canvas.

## How It Works

1. User navigates from Canvas B to Canvas A
2. `canvasId` change effect resets `hasInitializedStoreRef = null` and
sets `pendingStoreReinitRef = true`
3. React Query serves cached Canvas A data with `isFetching: true`
(background refetch triggered due to `staleTime: 0`)
4. Store initialization effect: ref mismatch → **initializes immediately
from cached data** (no loading gap); since `canvasFetching` is `true`,
`pendingStoreReinitRef` stays `true`
5. Background refetch completes: fresh Canvas A data arrives,
`canvasFetching` becomes `false`
6. Store initialization effect: `pendingStoreReinitRef` is `true` and
not fetching → **re-initializes from fresh data**, clears the flag
7. WebSocket events continue to merge correctly with the fresh baseline
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-f749f273-d24d-402e-b25e-9224f558c054"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-f749f273-d24d-402e-b25e-9224f558c054"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
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.

Stale canvas events shown when switching between canvases without page refresh

2 participants