Skip to content

Commit 3a143bc

Browse files
committed
refactor(workflows): collapse duplicate workflow-state cache
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.
1 parent 80fcb0e commit 3a143bc

3 files changed

Lines changed: 52 additions & 15 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { requestJson } from '@/lib/api/client/request'
2+
import {
3+
type GetWorkflowResponseData,
4+
getWorkflowStateContract,
5+
} from '@/lib/api/contracts/workflows'
6+
7+
/**
8+
* Fetches the full workflow envelope (in-state slice, deployment status,
9+
* variables, and row metadata) for a single workflow from GET
10+
* `/api/workflows/[id]`.
11+
*
12+
* Single source of truth for the `workflowKeys.state(id)` cache entry: the
13+
* registry store hydrates it via `fetchQuery` (always-fresh, in-flight
14+
* deduped) and `useWorkflowState`/`useWorkflowStates` project the mapped
15+
* `WorkflowState` out of the same entry with `select`, so this endpoint has
16+
* exactly one cache entry across the store and the hooks.
17+
*
18+
* Lives in a standalone util (rather than `hooks/queries/workflows.ts`) so the
19+
* registry store can import it without creating a store ↔ query-hook import
20+
* cycle.
21+
*/
22+
export async function fetchWorkflowEnvelope(
23+
workflowId: string,
24+
signal?: AbortSignal
25+
): Promise<GetWorkflowResponseData> {
26+
const { data } = await requestJson(getWorkflowStateContract, {
27+
params: { id: workflowId },
28+
signal,
29+
})
30+
return data
31+
}

apps/sim/hooks/queries/workflows.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
createWorkflowContract,
1919
deleteWorkflowContract,
2020
duplicateWorkflowContract,
21-
getWorkflowStateContract,
21+
type GetWorkflowResponseData,
2222
type ImportWorkflowAsSuperuserBody,
2323
type ImportWorkflowAsSuperuserResponse,
2424
importWorkflowAsSuperuserContract,
@@ -28,6 +28,7 @@ import {
2828
} from '@/lib/api/contracts/workflows'
2929
import { deploymentKeys } from '@/hooks/queries/deployments'
3030
import { fetchDeploymentVersionState } from '@/hooks/queries/utils/fetch-deployment-version-state'
31+
import { fetchWorkflowEnvelope } from '@/hooks/queries/utils/fetch-workflow-envelope'
3132
import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
3233
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
3334
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
@@ -49,14 +50,11 @@ const logger = createLogger('WorkflowQueries')
4950

5051
export { type WorkflowQueryScope, workflowKeys } from '@/hooks/queries/utils/workflow-keys'
5152

52-
async function fetchWorkflowState(
53-
workflowId: string,
54-
signal?: AbortSignal
55-
): Promise<WorkflowState | null> {
56-
const { data } = await requestJson(getWorkflowStateContract, {
57-
params: { id: workflowId },
58-
signal,
59-
})
53+
/**
54+
* Projects the in-state slice of the workflow envelope into the canvas-facing
55+
* `WorkflowState` shape consumed by preview/editor surfaces.
56+
*/
57+
function mapWorkflowState(data: GetWorkflowResponseData): WorkflowState {
6058
const wireState = data.state
6159
return {
6260
...wireState,
@@ -70,11 +68,16 @@ async function fetchWorkflowState(
7068
* Fetches the full workflow state for a single workflow.
7169
* Used by workflow blocks to show a preview of the child workflow
7270
* and as a base query for input fields extraction.
71+
*
72+
* Derives the mapped `WorkflowState` from the shared envelope query via
73+
* `select`, so it shares one cache entry (and one request) with the registry
74+
* store's hydration and with `useWorkflowStates`.
7375
*/
7476
export function useWorkflowState(workflowId: string | undefined) {
7577
return useQuery({
7678
queryKey: workflowKeys.state(workflowId),
77-
queryFn: workflowId ? ({ signal }) => fetchWorkflowState(workflowId, signal) : skipToken,
79+
queryFn: workflowId ? ({ signal }) => fetchWorkflowEnvelope(workflowId, signal) : skipToken,
80+
select: mapWorkflowState,
7881
staleTime: 30 * 1000,
7982
})
8083
}
@@ -93,7 +96,8 @@ export function useWorkflowStates(
9396
const results = useQueries({
9497
queries: uniqueIds.map((id) => ({
9598
queryKey: workflowKeys.state(id),
96-
queryFn: ({ signal }: { signal?: AbortSignal }) => fetchWorkflowState(id, signal),
99+
queryFn: ({ signal }: { signal?: AbortSignal }) => fetchWorkflowEnvelope(id, signal),
100+
select: mapWorkflowState,
97101
staleTime: 30 * 1000,
98102
})),
99103
})

apps/sim/stores/workflows/registry/store.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { createLogger } from '@sim/logger'
22
import { generateRandomHex } from '@sim/utils/random'
33
import { create } from 'zustand'
44
import { devtools } from 'zustand/middleware'
5-
import { requestJson } from '@/lib/api/client/request'
6-
import { getWorkflowStateContract } from '@/lib/api/contracts/workflows'
75
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
86
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
97
import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments'
108
import { deploymentKeys } from '@/hooks/queries/deployments'
9+
import { fetchWorkflowEnvelope } from '@/hooks/queries/utils/fetch-workflow-envelope'
1110
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
11+
import { workflowKeys } from '@/hooks/queries/utils/workflow-keys'
1212
import { useOperationQueueStore } from '@/stores/operation-queue/store'
1313
import { useVariablesStore } from '@/stores/variables/store'
1414
import type { Variable } from '@/stores/variables/types'
@@ -98,8 +98,10 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
9898
}))
9999

100100
try {
101-
const { data: workflowData } = await requestJson(getWorkflowStateContract, {
102-
params: { id: workflowId },
101+
const workflowData = await getQueryClient().fetchQuery({
102+
queryKey: workflowKeys.state(workflowId),
103+
queryFn: ({ signal }) => fetchWorkflowEnvelope(workflowId, signal),
104+
staleTime: 0,
103105
})
104106

105107
const deployedAt = workflowData.deployedAt ? workflowData.deployedAt.toISOString() : null

0 commit comments

Comments
 (0)