Skip to content

refactor(frontend-arch): migrate server state to React Query, collapse duplicate workflow-state cache, granular error boundaries#5168

Merged
waleedlatif1 merged 7 commits into
stagingfrom
chore/frontend-arch-cleanup
Jun 22, 2026
Merged

refactor(frontend-arch): migrate server state to React Query, collapse duplicate workflow-state cache, granular error boundaries#5168
waleedlatif1 merged 7 commits into
stagingfrom
chore/frontend-arch-cleanup

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • SessionProvider → React Query — replace the hand-rolled useState/useEffect/loadSession with a useSessionQuery() hook. Context shape is byte-identical ({ data, isPending, error, refetch }) so all 34 consumers are untouched. The ?upgraded path still forces a fresh DB read via getSession({ disableCookieCache: true }) + setQueryData (refetch can't pass that flag), now guarded by cancelQueries so a late-resolving stale mount fetch can't clobber the fresh session.
  • Collapse the duplicate workflow-state cache — the registry store fetched the GET /api/workflows/[id] envelope inline while useWorkflowState cached the same endpoint's mapped state separately (two requests, two shapes, never reconciled). Now one request + one cache entry keyed by workflowKeys.state(id): the store hydrates it via fetchQuery({ staleTime: 0 }) (preserving always-refetch, incl. the socket refresh path) and the hooks derive the mapped WorkflowState via select.
  • Granular error boundaries — add error.tsx to the logs, knowledge, and files panels so a crash scopes to the panel instead of the whole workspace shell (reuses the shared ErrorState).
  • Unsubscribe page → React Query — replace 5 useState + effect + inline fetch with useUnsubscribe query + useUnsubscribeMutation; contract change is additive (named type aliases only).

Type of Change

  • Improvement / refactor

Testing

  • Added 16 targeted tests (all passing, Biome-clean): session upgrade-path ordering (fresh wins over a late stale mount query — mutation-verified the cancelQueries guard), workflow-state cache collapse (single shared entry, always-refetch, request-id staleness guard), unsubscribe query gating + mutation cache reconcile, and the logs error boundary (first ErrorState coverage).
  • tsc 0 errors, bun run lint, check:react-query, check:api-validation all green.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…uery

Replace the hand-rolled useState/useEffect/loadSession session loading in
SessionProvider with a useSessionQuery() React Query hook. The SessionContext
shape is unchanged ({ data, isPending, error, refetch }) so no consumer changes.

The 'upgraded' path still forces a fresh DB read via
client.getSession({ query: { disableCookieCache: true } }) (refetch() cannot
pass disableCookieCache) and writes the result via queryClient.setQueryData,
then invalidates ['organizations']/['subscription'] as before.
The registry store fetched the GET /api/workflows/[id] envelope inline via
requestJson while useWorkflowState cached the same endpoint's mapped state
under workflowKeys.state(id) — two requests, two cache shapes, never
reconciled.

Collapse to one request + one cache entry keyed by workflowKeys.state(id):

- Add hooks/queries/utils/fetch-workflow-envelope.ts: a standalone
  fetchWorkflowEnvelope(id, signal) returning the full GetWorkflowResponseData.
  Standalone (not in workflows.ts) to avoid a store -> query-hook import cycle.
- useWorkflowState/useWorkflowStates now query the envelope and derive the
  mapped WorkflowState via select (mapWorkflowState), so consumers see the
  identical mapped shape from the shared entry.
- The store's loadWorkflowState reads via getQueryClient().fetchQuery({
  staleTime: 0 }) instead of raw requestJson — always-fresh (preserving the
  prior always-fetch boot/refresh semantics, incl. the socket
  handle-resource-event refresh path that has no separate state
  invalidation), in-flight deduped, writing into the same cache entry the
  hooks read.

Request-id staleness guard, deployment-cache priming, cross-store projection,
and the active-workflow-changed event are all preserved unchanged.
… files panels

Scope a crash in one workspace panel to that panel instead of the whole
workspace shell. Each boundary reuses the shared ErrorState component and
mirrors the existing tables/settings error.tsx convention.
Replace the hand-rolled useState+useEffect+requestJson server-state in the
unsubscribe page with React Query hooks. Add useUnsubscribe (validation/load
query, keyed by email+token, auto-runs on mount via enabled) and
useUnsubscribeMutation (unsubscribe action, reconciles cached preferences on
success) in hooks/queries/unsubscribe.ts with a hierarchical key factory.

Export UnsubscribeData/UnsubscribeActionResponse/UnsubscribeType type aliases
from the existing user contract; loading/error/success now derive from the
query and mutation objects with no local server-state mirror.
…lapse, unsubscribe, error boundary

Add targeted tests for the four frontend-architecture refactors:
- session-provider: upgrade-path ordering — fresh disableCookieCache read wins
  over a late-resolving stale mount query (proves the cancelQueries guard)
- fetch-workflow-envelope + registry store: single shared state(id) cache entry,
  always-refetch (staleTime 0), request-id staleness guard
- unsubscribe: query enable-gating + mutation cache reconcile
- logs error boundary: renders ErrorState + reset wiring (also first ErrorState coverage)
@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 22, 2026 7:20pm

Request Review

@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches auth session hydration after billing upgrades and core workflow registry loading; behavior is guarded by tests but race/cache ordering affects sign-in and editor state.

Overview
Moves session, unsubscribe, and workflow loading onto React Query while keeping existing consumer contracts where it matters.

SessionSessionProvider drops local loadSession state in favor of useSessionQuery (sessionKeys, 5‑minute staleTime, retry: false). Context stays { data, isPending, error, refetch }. AppSession lives in session-response.ts. After ?upgraded=true, the provider still forces getSession({ disableCookieCache: true }), writes via cancelQueries + setQueryData, invalidates organizations/subscription, and strips the query param; failed upgrade refresh no longer signs users out.

Workflow state — One cache entry per workflow on workflowKeys.state(id): shared fetchWorkflowEnvelope, registry loadWorkflowState uses fetchQuery({ staleTime: 0 }), and useWorkflowState / useWorkflowStates project canvas state with select.

Unsubscribe — Page uses useUnsubscribe + useUnsubscribeMutation (optimistic preference updates in cache) instead of inline effects.

Resilienceerror.tsx for logs, knowledge, and files panels via shared ErrorState. New tests cover upgrade ordering, cache collapse, unsubscribe, and logs error UI.

Reviewed by Cursor Bugbot for commit 7c92041. Configure here.

Comment thread apps/sim/app/_shell/providers/session-provider.tsx
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 650db2d. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR refactors four areas of server-state management toward React Query, eliminates a duplicate HTTP request for workflow state, and adds panel-scoped error boundaries.

  • SessionProvider replaces hand-rolled useState/useEffect with useSessionQuery(); the ?upgraded path now guards against stale-mount race conditions with cancelQueriessetQueryData, and AppSession is moved to lib/auth/session-response.ts to break the circular import.
  • Workflow-state cache collapse unifies the registry store's inline requestJson and useWorkflowState/useWorkflowStates hooks into a single workflowKeys.state(id) cache entry: the store hydrates via fetchQuery({ staleTime: 0 }) and the hooks derive WorkflowState through select: mapWorkflowState.
  • Granular error boundaries (error.tsx) are added for the logs, files, and knowledge panels so a crash in one panel no longer collapses the entire workspace shell; the unsubscribe page's five useState + effect + inline fetch are replaced with useUnsubscribe / useUnsubscribeMutation, with 16 targeted tests covering upgrade-path ordering, cache-collapse invariants, and the new error boundaries.

Confidence Score: 5/5

Safe to merge — the refactor is behavior-preserving, the test suite covers the critical race-condition paths, and no existing consumers of the changed cache entries were left broken.

All four migration surfaces (session, workflow state, unsubscribe, error boundaries) are backed by targeted tests. The cancelQueries → setQueryData ordering in the upgrade path is verified by an abort-signal test that confirms the stale mount fetch cannot clobber the fresh session. The workflow-state cache collapse is validated to produce exactly one shared entry. The only asymmetry between the store's staleTime: 0 and the hooks' staleTime: 30s is intentional and works correctly because fetchQuery updates the shared cache entry that hook observers automatically read from.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/_shell/providers/session-provider.tsx Replaces hand-rolled state with useSessionQuery; upgrade path correctly guards against stale-mount clobber via cancelQueries → setQueryData
apps/sim/hooks/queries/workflows.ts useWorkflowState and useWorkflowStates now share one cache entry (GetWorkflowResponseData) with the registry store and derive WorkflowState via select: mapWorkflowState; no direct cache readers broken
apps/sim/stores/workflows/registry/store.ts loadWorkflowState migrated to fetchQuery({ staleTime: 0 }) to hydrate the shared workflow-state cache entry; staleness guard (requestId / workflowId comparison) preserved
apps/sim/hooks/queries/unsubscribe.ts New useUnsubscribe query + useUnsubscribeMutation; cache is reconciled in onSuccess with correct preference-key mapping
apps/sim/hooks/queries/utils/fetch-workflow-envelope.ts Standalone util to fetch the workflow envelope; lives outside hooks/queries/workflows.ts to avoid a store ↔ hook import cycle
apps/sim/lib/auth/session-response.ts AppSession type moved here from session-provider.tsx, breaking the provider ↔ hook circular import

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant SessionProvider
    participant useSessionQuery
    participant QueryClient
    participant BetterAuth

    Browser->>SessionProvider: "mount (URL has ?upgraded=true)"
    SessionProvider->>useSessionQuery: subscribe (stale cookie-cached fetch starts)
    useSessionQuery->>BetterAuth: getSession() [cookie-cached, with AbortSignal]
    SessionProvider->>SessionProvider: strip ?upgraded from URL
    SessionProvider->>BetterAuth: "getSession({disableCookieCache:true})"
    BetterAuth-->>SessionProvider: fresh session
    SessionProvider->>QueryClient: cancelQueries(sessionKeys.detail()) → abort stale signal
    BetterAuth-->>useSessionQuery: AbortError (signal fired)
    SessionProvider->>QueryClient: setQueryData(sessionKeys.detail(), fresh)
    QueryClient-->>SessionProvider: "context data = fresh session"
    SessionProvider->>QueryClient: invalidateQueries(['organizations'])
    SessionProvider->>QueryClient: invalidateQueries(['subscription'])
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant SessionProvider
    participant useSessionQuery
    participant QueryClient
    participant BetterAuth

    Browser->>SessionProvider: "mount (URL has ?upgraded=true)"
    SessionProvider->>useSessionQuery: subscribe (stale cookie-cached fetch starts)
    useSessionQuery->>BetterAuth: getSession() [cookie-cached, with AbortSignal]
    SessionProvider->>SessionProvider: strip ?upgraded from URL
    SessionProvider->>BetterAuth: "getSession({disableCookieCache:true})"
    BetterAuth-->>SessionProvider: fresh session
    SessionProvider->>QueryClient: cancelQueries(sessionKeys.detail()) → abort stale signal
    BetterAuth-->>useSessionQuery: AbortError (signal fired)
    SessionProvider->>QueryClient: setQueryData(sessionKeys.detail(), fresh)
    QueryClient-->>SessionProvider: "context data = fresh session"
    SessionProvider->>QueryClient: invalidateQueries(['organizations'])
    SessionProvider->>QueryClient: invalidateQueries(['subscription'])
Loading

Reviews (5): Last reviewed commit: "refactor(session): break provider<->hook..." | Re-trigger Greptile

Comment thread apps/sim/hooks/queries/session.ts
Comment thread apps/sim/app/_shell/providers/session-provider.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR migrates three server-state islands to React Query — the session provider, the unsubscribe page, and the workflow-state cache — and adds granular Next.js error boundaries for the logs, files, and knowledge panels.

  • SessionProvider: hand-rolled useState/useEffect/loadSession replaced with useSessionQuery; the ?upgraded=true path correctly guards against stale-mount clobber via cancelQueries + setQueryData.
  • Workflow-state cache collapse: the registry store's inline requestJson call and useWorkflowState's separate cache entry are unified under one workflowKeys.state(id) key — the store hydrates via fetchQuery({ staleTime: 0 }) and the hooks derive WorkflowState through a stable module-level select.
  • Unsubscribe page: 5 useState + effect + inline fetch replaced with useUnsubscribe query and useUnsubscribeMutation with correct optimistic cache reconciliation for all preference-key variants.

Confidence Score: 4/5

The refactor is well-structured and the core cache-collapse and upgrade-path logic are correct; the main watch-out is in the session query, which now silently retries failed auth checks up to three times before surfacing an error.

The workflow-state cache collapse, unsubscribe migration, and error boundary additions are clean and well-tested. The session path has two smaller issues: useSessionQuery inherits React Query's default 3-retry policy (the old code failed fast), and refreshAfterUpgrade returns the fetched session when cancelled — the caller discards it, but the asymmetric return type makes the cancellation contract harder to read. Neither causes incorrect behavior today, but the retry change is a silent behavioural difference that could delay error display on auth failure.

apps/sim/hooks/queries/session.ts and apps/sim/app/_shell/providers/session-provider.tsx warrant a second look — the session query lacks an explicit retry policy and the circular type dependency between the two files should be addressed.

Important Files Changed

Filename Overview
apps/sim/hooks/queries/session.ts New React Query hook for session fetching; missing retry: false changes failure behavior (retries 3x vs. the old fail-fast) and imports AppSession from the provider, creating a circular type dependency.
apps/sim/app/_shell/providers/session-provider.tsx Hand-rolled useState/useEffect session loading replaced with useSessionQuery; upgrade path rewritten to use cancelQueries + setQueryData; refreshAfterUpgrade returns the fresh value on cancellation even though the caller always discards it.
apps/sim/stores/workflows/registry/store.ts Direct requestJson call replaced with getQueryClient().fetchQuery({ staleTime: 0 }) so the registry hydrates the shared workflowKeys.state(id) cache entry; staleness guard and request-id deduplication logic unchanged and correct.
apps/sim/hooks/queries/workflows.ts useWorkflowState and useWorkflowStates now use fetchWorkflowEnvelope as queryFn and mapWorkflowState as a stable module-level select, collapsing two cache entries into one with correct structural sharing.
apps/sim/hooks/queries/utils/fetch-workflow-envelope.ts New utility that fetches the full workflow envelope from GET /api/workflows/[id]; extracted to a standalone file to avoid a store-to-query-hook import cycle.
apps/sim/hooks/queries/unsubscribe.ts New useUnsubscribe query + useUnsubscribeMutation hook; cache reconciliation on success correctly handles the 'all' case and specific type keys; preference key derivation is type-safe via UnsubscribeType.
apps/sim/app/unsubscribe/unsubscribe.tsx 5 useState + effect + inline fetch replaced with useUnsubscribe/useUnsubscribeMutation; loading/error/processing/unsubscribed states all derived from query/mutation state correctly.
apps/sim/lib/api/contracts/user.ts Additive-only change: exports named type aliases (UnsubscribeData, UnsubscribeActionResponse, UnsubscribeBody, UnsubscribeType) so consumers do not need to import zod or reconstruct these types inline.
apps/sim/app/workspace/[workspaceId]/logs/error.tsx New Next.js error boundary for the logs panel using the shared ErrorState component; scopes crash to the panel rather than the workspace shell.
apps/sim/app/workspace/[workspaceId]/files/error.tsx New Next.js error boundary for the files panel, mirrors the logs error boundary pattern.
apps/sim/app/workspace/[workspaceId]/knowledge/error.tsx New Next.js error boundary for the knowledge panel, mirrors the logs error boundary pattern.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Browser
    participant SessionProvider
    participant RQ as React Query Cache
    participant AuthClient as Better Auth Client

    Note over Browser,AuthClient: Normal mount (no ?upgraded)
    SessionProvider->>RQ: useSessionQuery() subscribes
    RQ->>AuthClient: fetchSession() if stale
    AuthClient-->>RQ: AppSession data
    RQ-->>SessionProvider: data, isPending, error, refetch

    Note over Browser,AuthClient: Plan upgrade redirect (?upgraded=true)
    Browser->>SessionProvider: "mount with ?upgraded=true in URL"
    SessionProvider->>Browser: window.history.replaceState strip param
    SessionProvider->>RQ: cancelQueries sessionKeys.detail
    SessionProvider->>AuthClient: getSession disableCookieCache true
    AuthClient-->>SessionProvider: fresh AppSession
    SessionProvider->>RQ: setQueryData fresh session
    Note right of RQ: Stale mount query cancelled so fresh data wins

    Note over Browser,AuthClient: Workflow state hydration registry store
    SessionProvider->>RQ: fetchQuery workflowKeys.state id staleTime 0
    RQ->>AuthClient: fetchWorkflowEnvelope id
    AuthClient-->>RQ: GetWorkflowResponseData stored in cache
    RQ-->>SessionProvider: raw envelope
    Note right of RQ: useWorkflowState subscribers get mapWorkflowState via select
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Browser
    participant SessionProvider
    participant RQ as React Query Cache
    participant AuthClient as Better Auth Client

    Note over Browser,AuthClient: Normal mount (no ?upgraded)
    SessionProvider->>RQ: useSessionQuery() subscribes
    RQ->>AuthClient: fetchSession() if stale
    AuthClient-->>RQ: AppSession data
    RQ-->>SessionProvider: data, isPending, error, refetch

    Note over Browser,AuthClient: Plan upgrade redirect (?upgraded=true)
    Browser->>SessionProvider: "mount with ?upgraded=true in URL"
    SessionProvider->>Browser: window.history.replaceState strip param
    SessionProvider->>RQ: cancelQueries sessionKeys.detail
    SessionProvider->>AuthClient: getSession disableCookieCache true
    AuthClient-->>SessionProvider: fresh AppSession
    SessionProvider->>RQ: setQueryData fresh session
    Note right of RQ: Stale mount query cancelled so fresh data wins

    Note over Browser,AuthClient: Workflow state hydration registry store
    SessionProvider->>RQ: fetchQuery workflowKeys.state id staleTime 0
    RQ->>AuthClient: fetchWorkflowEnvelope id
    AuthClient-->>RQ: GetWorkflowResponseData stored in cache
    RQ-->>SessionProvider: raw envelope
    Note right of RQ: useWorkflowState subscribers get mapWorkflowState via select
Loading

Reviews (2): Last reviewed commit: "test(frontend-arch): cover session race ..." | Re-trigger Greptile

Comment thread apps/sim/hooks/queries/session.ts
Comment thread apps/sim/hooks/queries/session.ts
Comment thread apps/sim/app/_shell/providers/session-provider.tsx
- Reconcile plan surfaces after upgrade even when the fresh disableCookieCache
  read fails: invalidate ['organizations']/['subscription'] regardless of the
  bypass-read outcome (they read server truth, not the cookie cache). The valid
  cookie-cached session is still served, so a transient failure no longer signs
  the user out or leaves the just-upgraded plan looking stale. Org-activate
  fallback stays gated on having a session.
- Use a bare return in the cancelled branch of refreshAfterUpgrade (the caller
  discards the value) for clearer intent; caller coerces with ?? null.
- Make the upgrade tests deterministic: the mount mock honors the abort signal
  like the real fetch-backed client, and assertions read the query cache (the
  state cancelQueries/setQueryData/invalidation actually govern) instead of the
  async-rendered context value.
…n query

Address review feedback:
- Move the AppSession type to lib/auth/session-response.ts (the module that
  produces it) so useSessionQuery and SessionProvider both import it from there,
  eliminating the provider <-> query-hook import cycle.
- Add retry: false to useSessionQuery, restoring the prior fail-fast contract
  (the global QueryClient default is retry: 1; an auth failure should surface
  immediately rather than retry a request that won't succeed).
- Return null (not the fetched value) from refreshAfterUpgrade's cancelled
  branch to make the cancellation contract explicit.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 7c92041. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

1 similar comment
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1 waleedlatif1 merged commit e96b150 into staging Jun 22, 2026
16 checks passed
@waleedlatif1 waleedlatif1 deleted the chore/frontend-arch-cleanup branch June 22, 2026 19:44
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