fix: handle background command rejections in App (#2049)
`runWithCommandAuthRecovery` rethrows anything that isn't an `AuthRecoveryRequiredError`, and 17 of its 18 call sites were bare `void runWithCommandAuthRecovery(...)` — so any non-auth failure of a user-initiated list reload (Retry, Load next page, the pagination-mode toggle) became an unhandled rejection in the browser. Neither gate that should catch it could: `void` is the sanctioned suppression for `no-floating-promises`, and `smoke:web:browser` — which hard-fails on exactly this signal — never connects to a server. Add `runCommandInBackground`, a fire-and-forget wrapper that owns the rejection, and route every background command through it, so a new call site cannot reintroduce the gap by omission. An optional `errorTitle` picks the reporting: omitted where the list panel already renders the failure with a Retry (a toast would duplicate it), passed where nothing else records it — `logging/setLevel` and the tasks refresh. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
7be5875dc5
commit
f96c4583ee
1 changed files with 227 additions and 65 deletions
|
|
@ -2066,6 +2066,46 @@ function App() {
|
|||
[inspectorClient, activeServerId, handleCommandScopedAuthRecovery],
|
||||
);
|
||||
|
||||
/**
|
||||
* Fire-and-forget form of {@link runWithCommandAuthRecovery}, for a command
|
||||
* whose caller has nothing to await (a click handler, a mode toggle).
|
||||
*
|
||||
* The wrapper rethrows anything that is not an `AuthRecoveryRequiredError`,
|
||||
* so a bare `void runWithCommandAuthRecovery(...)` turns every non-auth
|
||||
* failure — a transport error, a rejected `tools/list` — into an unhandled
|
||||
* rejection in the browser (#2049). Routing every background command through
|
||||
* here is what keeps a new call site from reintroducing that gap by
|
||||
* omission: there is no `void` to forget.
|
||||
*
|
||||
* `errorTitle` picks the reporting, and the choice is per call site:
|
||||
*
|
||||
* - **Omit it** when the operation's own state already renders the failure —
|
||||
* the list loads, whose managed/paged stores record the error and whose
|
||||
* panel shows it with a Retry. A toast there would only duplicate what the
|
||||
* user is already looking at, so the rejection is swallowed deliberately.
|
||||
* - **Pass one** when nothing else records the failure, and the user would
|
||||
* otherwise see the command silently do nothing.
|
||||
*/
|
||||
const runCommandInBackground = useCallback(
|
||||
(
|
||||
operation: () => Promise<unknown>,
|
||||
source: StepUpSource,
|
||||
errorTitle?: string,
|
||||
): void => {
|
||||
void runWithCommandAuthRecovery(operation, source).catch(
|
||||
(err: unknown) => {
|
||||
if (!errorTitle) return;
|
||||
notifications.show({
|
||||
title: errorTitle,
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
[runWithCommandAuthRecovery],
|
||||
);
|
||||
|
||||
const resumePendingReauth = useCallback(
|
||||
async (pending: PendingReauth) => {
|
||||
if (reauthResumeInProgressRef.current) {
|
||||
|
|
@ -3474,12 +3514,16 @@ function App() {
|
|||
(level: LoggingLevel) => {
|
||||
setCurrentLogLevel(level);
|
||||
if (!inspectorClient) return;
|
||||
void runWithCommandAuthRecovery(
|
||||
// Nothing else records a `logging/setLevel` failure, and the optimistic
|
||||
// level above already moved — so report it, or the selector would sit on
|
||||
// a level the server never accepted with no explanation.
|
||||
runCommandInBackground(
|
||||
() => inspectorClient.setLoggingLevel(level),
|
||||
"ambient",
|
||||
"Failed to set log level",
|
||||
);
|
||||
},
|
||||
[inspectorClient, runWithCommandAuthRecovery],
|
||||
[inspectorClient, runCommandInBackground],
|
||||
);
|
||||
|
||||
// Modern era (#1629): no request is sent — the client stores the level and
|
||||
|
|
@ -3502,51 +3546,39 @@ function App() {
|
|||
// (the managed state lit it on `list_changed`; nothing else clears it in
|
||||
// paginated mode) (#1721).
|
||||
clearToolsListChanged();
|
||||
void runWithCommandAuthRecovery(
|
||||
() => toolsPagination.onRefresh(),
|
||||
"ambient",
|
||||
);
|
||||
runCommandInBackground(() => toolsPagination.onRefresh(), "ambient");
|
||||
} else {
|
||||
void runWithCommandAuthRecovery(() => refreshTools(), "ambient");
|
||||
runCommandInBackground(() => refreshTools(), "ambient");
|
||||
}
|
||||
}, [
|
||||
paginatedLists,
|
||||
toolsPagination,
|
||||
refreshTools,
|
||||
clearToolsListChanged,
|
||||
runWithCommandAuthRecovery,
|
||||
runCommandInBackground,
|
||||
]);
|
||||
const onRefreshPrompts = useCallback(() => {
|
||||
if (paginatedLists) {
|
||||
clearPromptsListChanged();
|
||||
void runWithCommandAuthRecovery(
|
||||
() => promptsPagination.onRefresh(),
|
||||
"ambient",
|
||||
);
|
||||
runCommandInBackground(() => promptsPagination.onRefresh(), "ambient");
|
||||
} else {
|
||||
void runWithCommandAuthRecovery(() => refreshPrompts(), "ambient");
|
||||
runCommandInBackground(() => refreshPrompts(), "ambient");
|
||||
}
|
||||
}, [
|
||||
paginatedLists,
|
||||
promptsPagination,
|
||||
refreshPrompts,
|
||||
clearPromptsListChanged,
|
||||
runWithCommandAuthRecovery,
|
||||
runCommandInBackground,
|
||||
]);
|
||||
const onRefreshResources = useCallback(() => {
|
||||
if (paginatedLists) {
|
||||
clearResourcesListChanged();
|
||||
void runWithCommandAuthRecovery(
|
||||
() => resourcesPagination.onRefresh(),
|
||||
"ambient",
|
||||
);
|
||||
runCommandInBackground(() => resourcesPagination.onRefresh(), "ambient");
|
||||
// Resource templates always use the managed (aggregate) path.
|
||||
void runWithCommandAuthRecovery(
|
||||
() => refreshResourceTemplates(),
|
||||
"ambient",
|
||||
);
|
||||
runCommandInBackground(() => refreshResourceTemplates(), "ambient");
|
||||
} else {
|
||||
void runWithCommandAuthRecovery(async () => {
|
||||
runCommandInBackground(async () => {
|
||||
await refreshResources();
|
||||
await refreshResourceTemplates();
|
||||
}, "ambient");
|
||||
|
|
@ -3557,7 +3589,7 @@ function App() {
|
|||
refreshResources,
|
||||
refreshResourceTemplates,
|
||||
clearResourcesListChanged,
|
||||
runWithCommandAuthRecovery,
|
||||
runCommandInBackground,
|
||||
]);
|
||||
// The per-list sidebar toggle edits the server-wide `paginatedLists` setting:
|
||||
// optimistic override for an instant flip, live push so the managed state's
|
||||
|
|
@ -3581,22 +3613,13 @@ function App() {
|
|||
// Wrap in ambient auth recovery so a mid-session 401 triggers re-auth
|
||||
// rather than surfacing raw, matching the all-pages refresh path.
|
||||
if (value) {
|
||||
void runWithCommandAuthRecovery(
|
||||
() => loadToolsPage(undefined),
|
||||
"ambient",
|
||||
);
|
||||
void runWithCommandAuthRecovery(
|
||||
() => loadPromptsPage(undefined),
|
||||
"ambient",
|
||||
);
|
||||
void runWithCommandAuthRecovery(
|
||||
() => loadResourcesPage(undefined),
|
||||
"ambient",
|
||||
);
|
||||
runCommandInBackground(() => loadToolsPage(undefined), "ambient");
|
||||
runCommandInBackground(() => loadPromptsPage(undefined), "ambient");
|
||||
runCommandInBackground(() => loadResourcesPage(undefined), "ambient");
|
||||
} else {
|
||||
void runWithCommandAuthRecovery(() => refreshTools(), "ambient");
|
||||
void runWithCommandAuthRecovery(() => refreshPrompts(), "ambient");
|
||||
void runWithCommandAuthRecovery(() => refreshResources(), "ambient");
|
||||
runCommandInBackground(() => refreshTools(), "ambient");
|
||||
runCommandInBackground(() => refreshPrompts(), "ambient");
|
||||
runCommandInBackground(() => refreshResources(), "ambient");
|
||||
}
|
||||
}
|
||||
void updateServerSettings(activeServerId, next).catch((err: unknown) => {
|
||||
|
|
@ -3625,34 +3648,24 @@ function App() {
|
|||
refreshTools,
|
||||
refreshPrompts,
|
||||
refreshResources,
|
||||
runWithCommandAuthRecovery,
|
||||
runCommandInBackground,
|
||||
],
|
||||
);
|
||||
// Wrap Load-next-page in ambient auth recovery too, so a paginated
|
||||
// paginated fetch that hits a 401 recovers like the all-pages path (#1721).
|
||||
const onLoadMoreTools = useCallback(
|
||||
() =>
|
||||
void runWithCommandAuthRecovery(
|
||||
() => toolsPagination.onLoadMore(),
|
||||
"ambient",
|
||||
),
|
||||
[toolsPagination, runWithCommandAuthRecovery],
|
||||
() => runCommandInBackground(() => toolsPagination.onLoadMore(), "ambient"),
|
||||
[toolsPagination, runCommandInBackground],
|
||||
);
|
||||
const onLoadMorePrompts = useCallback(
|
||||
() =>
|
||||
void runWithCommandAuthRecovery(
|
||||
() => promptsPagination.onLoadMore(),
|
||||
"ambient",
|
||||
),
|
||||
[promptsPagination, runWithCommandAuthRecovery],
|
||||
runCommandInBackground(() => promptsPagination.onLoadMore(), "ambient"),
|
||||
[promptsPagination, runCommandInBackground],
|
||||
);
|
||||
const onLoadMoreResources = useCallback(
|
||||
() =>
|
||||
void runWithCommandAuthRecovery(
|
||||
() => resourcesPagination.onLoadMore(),
|
||||
"ambient",
|
||||
),
|
||||
[resourcesPagination, runWithCommandAuthRecovery],
|
||||
runCommandInBackground(() => resourcesPagination.onLoadMore(), "ambient"),
|
||||
[resourcesPagination, runCommandInBackground],
|
||||
);
|
||||
const toolsPaginationControls: ListPaginationControlsProps = {
|
||||
paginated: toolsPagination.paginated,
|
||||
|
|
@ -3676,16 +3689,12 @@ function App() {
|
|||
onLoadMore: onLoadMoreResources,
|
||||
};
|
||||
const onRefreshTasks = useCallback(() => {
|
||||
void runWithCommandAuthRecovery(() => refreshTasks(), "ambient")?.catch(
|
||||
(err: unknown) => {
|
||||
notifications.show({
|
||||
title: "Failed to refresh tasks",
|
||||
message: err instanceof Error ? err.message : String(err),
|
||||
color: "red",
|
||||
});
|
||||
},
|
||||
runCommandInBackground(
|
||||
() => refreshTasks(),
|
||||
"ambient",
|
||||
"Failed to refresh tasks",
|
||||
);
|
||||
}, [refreshTasks, runWithCommandAuthRecovery]);
|
||||
}, [refreshTasks, runCommandInBackground]);
|
||||
|
||||
const onClearLogs = useCallback(() => {
|
||||
if (!messageLogState) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue