fix(web): stabilize useMutation results so TerminalPanel stops flooding /shell/resize
The mutationFn / onSuccess closures inside every useMutation in
process.ts were created fresh on every render, which made the
mutation result object unstable across renders. TerminalPanel's
terminal-init useEffect has [resize, workspaceId] as deps; with
"resize" changing on every render, the effect re-ran on every
render, which:
- disposed and recreated the xterm Terminal
- re-attached onResize (which fires fit() → onResize → mutate)
- 100ms debounce kept getting reset
- but the effect re-creating itself with new closures every
~16ms kept fit()'s pre-resize state oscillating, eventually
letting the debounce expire and firing the API
Result: /api/workspaces/:id/shell/resize was being spammed.
Fix: wrap every useMutation's mutationFn (and onSuccess where
present) in useCallback with explicit deps. useMutation now sees a
stable config and returns a stable result object. The terminal
effect's dep [resize, workspaceId] no longer changes per render
unless workspaceId actually changes.
Affects: useProcessStart/Stop/Restart, useShellStart/Stop/
Restart/Resize. All same pattern, all same risk.
This commit is contained in:
+38
-23
@@ -188,14 +188,19 @@ function useProcessActionMutation(
|
|||||||
action: "start" | "stop" | "restart",
|
action: "start" | "stop" | "restart",
|
||||||
): UseMutationResult<void, Error, string> {
|
): UseMutationResult<void, Error, string> {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
const mutationFn = useCallback(
|
||||||
mutationFn: (workspaceId: string) => postProcessAction(workspaceId, action),
|
(workspaceId: string) => postProcessAction(workspaceId, action),
|
||||||
onSuccess: (_, workspaceId) => {
|
[action],
|
||||||
|
);
|
||||||
|
const onSuccess = useCallback(
|
||||||
|
(_: void, workspaceId: string) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: processStatusQueryKey(workspaceId),
|
queryKey: processStatusQueryKey(workspaceId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
[queryClient],
|
||||||
|
);
|
||||||
|
return useMutation({ mutationFn, onSuccess });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -215,14 +220,13 @@ export function useShellStart(
|
|||||||
workspaceId: string | null,
|
workspaceId: string | null,
|
||||||
): UseMutationResult<ShellInfo, Error, void> {
|
): UseMutationResult<ShellInfo, Error, void> {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
const mutationFn = useCallback(async () => {
|
||||||
mutationFn: async () => {
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error("workspace id required");
|
throw new Error("workspace id required");
|
||||||
}
|
}
|
||||||
return postShellStart(workspaceId);
|
return postShellStart(workspaceId);
|
||||||
},
|
}, [workspaceId]);
|
||||||
onSuccess: () => {
|
const onSuccess = useCallback(() => {
|
||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: shellListQueryKey(workspaceId),
|
queryKey: shellListQueryKey(workspaceId),
|
||||||
@@ -230,22 +234,24 @@ export function useShellStart(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
||||||
});
|
});
|
||||||
},
|
}, [queryClient, workspaceId]);
|
||||||
});
|
return useMutation({ mutationFn, onSuccess });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShellStop(
|
export function useShellStop(
|
||||||
workspaceId: string | null,
|
workspaceId: string | null,
|
||||||
): UseMutationResult<void, Error, { shellId: string }> {
|
): UseMutationResult<void, Error, { shellId: string }> {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
const mutationFn = useCallback(
|
||||||
mutationFn: async ({ shellId }) => {
|
async ({ shellId }: { shellId: string }) => {
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error("workspace id required");
|
throw new Error("workspace id required");
|
||||||
}
|
}
|
||||||
return postShellStop(workspaceId, shellId);
|
return postShellStop(workspaceId, shellId);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
[workspaceId],
|
||||||
|
);
|
||||||
|
const onSuccess = useCallback(() => {
|
||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: shellListQueryKey(workspaceId),
|
queryKey: shellListQueryKey(workspaceId),
|
||||||
@@ -253,22 +259,24 @@ export function useShellStop(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
||||||
});
|
});
|
||||||
},
|
}, [queryClient, workspaceId]);
|
||||||
});
|
return useMutation({ mutationFn, onSuccess });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useShellRestart(
|
export function useShellRestart(
|
||||||
workspaceId: string | null,
|
workspaceId: string | null,
|
||||||
): UseMutationResult<ShellInfo, Error, { shellId: string }> {
|
): UseMutationResult<ShellInfo, Error, { shellId: string }> {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
const mutationFn = useCallback(
|
||||||
mutationFn: async ({ shellId }) => {
|
async ({ shellId }: { shellId: string }) => {
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error("workspace id required");
|
throw new Error("workspace id required");
|
||||||
}
|
}
|
||||||
return postShellRestart(workspaceId, shellId);
|
return postShellRestart(workspaceId, shellId);
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
[workspaceId],
|
||||||
|
);
|
||||||
|
const onSuccess = useCallback(() => {
|
||||||
if (!workspaceId) return;
|
if (!workspaceId) return;
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: shellListQueryKey(workspaceId),
|
queryKey: shellListQueryKey(workspaceId),
|
||||||
@@ -276,8 +284,8 @@ export function useShellRestart(
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
queryKey: ["workspaces", workspaceId, "shell", "status"],
|
||||||
});
|
});
|
||||||
},
|
}, [queryClient, workspaceId]);
|
||||||
});
|
return useMutation({ mutationFn, onSuccess });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postShellResize(
|
async function postShellResize(
|
||||||
@@ -303,14 +311,21 @@ export function useShellResize(
|
|||||||
workspaceId: string | null,
|
workspaceId: string | null,
|
||||||
shellId: string | null,
|
shellId: string | null,
|
||||||
): UseMutationResult<void, Error, { cols: number; rows: number }> {
|
): UseMutationResult<void, Error, { cols: number; rows: number }> {
|
||||||
return useMutation({
|
// Stable mutationFn: without useCallback, a fresh closure is created on
|
||||||
mutationFn: async ({ cols, rows }) => {
|
// every render, which makes useMutation's returned object unstable. That
|
||||||
|
// ripples into useEffect dep arrays in TerminalPanel and causes the
|
||||||
|
// terminal to be torn down and recreated on every render — which fires
|
||||||
|
// onResize in a tight loop and floods /shell/resize.
|
||||||
|
const mutationFn = useCallback(
|
||||||
|
async ({ cols, rows }: { cols: number; rows: number }) => {
|
||||||
if (!workspaceId || !shellId) {
|
if (!workspaceId || !shellId) {
|
||||||
throw new Error("workspace id and shell id required");
|
throw new Error("workspace id and shell id required");
|
||||||
}
|
}
|
||||||
return postShellResize(workspaceId, shellId, cols, rows);
|
return postShellResize(workspaceId, shellId, cols, rows);
|
||||||
},
|
},
|
||||||
});
|
[workspaceId, shellId],
|
||||||
|
);
|
||||||
|
return useMutation({ mutationFn });
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProcessWebSocketStatus =
|
export type ProcessWebSocketStatus =
|
||||||
|
|||||||
Reference in New Issue
Block a user