web: address #1670 review — observable deep-link failures + correct stale-config connect
- The ensure-phase addServer catch now records non-409 failures (read-only catalog, backend 5xx) on data-error-message instead of swallowing every error and leaving the deep link permanently stuck at the guard with no signal (review finding #1). - Split the update and connect phases across renders (new deepLinkUpdateRef): connect only once servers reflects the updated config, so onToggleConnection (which reads serversRef, synced by an earlier passive effect) builds the client from the fresh transport rather than the pre-update config (finding #2 — the exact case deepLinkConfigEquals was added for). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
0e93b13426
commit
0beb6e65af
1 changed files with 45 additions and 30 deletions
|
|
@ -1381,6 +1381,7 @@ function App() {
|
||||||
return [parsed, deepLinkParseStatus(search, parsed)];
|
return [parsed, deepLinkParseStatus(search, parsed)];
|
||||||
}, []);
|
}, []);
|
||||||
const deepLinkEnsureRef = useRef(false);
|
const deepLinkEnsureRef = useRef(false);
|
||||||
|
const deepLinkUpdateRef = useRef(false);
|
||||||
const deepLinkConnectRef = useRef(false);
|
const deepLinkConnectRef = useRef(false);
|
||||||
|
|
||||||
const showReAuthBanner = useCallback(
|
const showReAuthBanner = useCallback(
|
||||||
|
|
@ -2518,15 +2519,20 @@ function App() {
|
||||||
}, [inspectorClient, finalizeExplicitDisconnect]);
|
}, [inspectorClient, finalizeExplicitDisconnect]);
|
||||||
|
|
||||||
// Deep-link auto-connect (the URL-driven case of #1183). `useServers`
|
// Deep-link auto-connect (the URL-driven case of #1183). `useServers`
|
||||||
// hydrates asynchronously (initial `servers` is `[]`), so this effect runs
|
// hydrates asynchronously (initial `servers` is `[]`), so this effect runs in
|
||||||
// in two phases: first render attempts a one-shot `addServer` (a 409 means
|
// discrete phases keyed on what `servers` currently reflects, one per render:
|
||||||
// the row already exists on disk and hydration will surface it); a later
|
// 1. ensure — no row yet: one-shot `addServer`.
|
||||||
// render where `servers` actually contains the deep-link row then updates the
|
// 2. update — row present but its persisted config differs from the deep
|
||||||
// URL if it has changed and connects. Connecting in the same closure as
|
// link (a stale transport/url from an earlier load under the stable
|
||||||
// `addServer` would resolve against a stale (empty) `servers` and silently
|
// `deep-link` id): `updateServer`, then return so the effect re-runs.
|
||||||
// no-op. The OAuth callback path takes precedence; a deep link on
|
// 3. connect — row present AND its config already matches: connect.
|
||||||
// `/oauth/callback` would be a misconfiguration, and the callback handler
|
// Splitting update and connect across renders (rather than awaiting both in
|
||||||
// clears the URL.
|
// one closure) is what makes the connect correct: `onToggleConnection` reads
|
||||||
|
// the target from `serversRef`, which an earlier passive effect syncs from
|
||||||
|
// `servers` — so connecting only once `servers` reflects the updated config
|
||||||
|
// guarantees the client is built from the fresh transport, not the stale one.
|
||||||
|
// The OAuth callback path takes precedence; a deep link on `/oauth/callback`
|
||||||
|
// would be a misconfiguration, and the callback handler clears the URL.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deepLink) return;
|
if (!deepLink) return;
|
||||||
if (window.location.pathname === OAUTH_CALLBACK_PATH) return;
|
if (window.location.pathname === OAUTH_CALLBACK_PATH) return;
|
||||||
|
|
@ -2535,33 +2541,42 @@ function App() {
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
if (deepLinkEnsureRef.current) return;
|
if (deepLinkEnsureRef.current) return;
|
||||||
deepLinkEnsureRef.current = true;
|
deepLinkEnsureRef.current = true;
|
||||||
void addServer(deepLink.serverId, deepLink.serverConfig).catch(() => {
|
void addServer(deepLink.serverId, deepLink.serverConfig).catch((err) => {
|
||||||
// 409 = already on disk; hydration will surface it on a later render.
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
// A 409 ("already exists") means the row is on disk and hydration will
|
||||||
|
// surface it on a later render, so the connect phase still proceeds —
|
||||||
|
// swallow it. Any other failure (read-only catalog, backend 5xx) would
|
||||||
|
// otherwise leave the deep link permanently stuck at this guard with no
|
||||||
|
// signal, so record it on the machine-readable error surface.
|
||||||
|
if (!message.includes("already exists")) recordConnectError(message);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deepLinkConfigEquals(existing.config, deepLink.serverConfig)) {
|
||||||
|
if (deepLinkUpdateRef.current) return;
|
||||||
|
deepLinkUpdateRef.current = true;
|
||||||
|
void updateServer(
|
||||||
|
deepLink.serverId,
|
||||||
|
deepLink.serverId,
|
||||||
|
deepLink.serverConfig,
|
||||||
|
).catch((err) => {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
recordConnectError(message);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deepLinkConnectRef.current) return;
|
if (deepLinkConnectRef.current) return;
|
||||||
deepLinkConnectRef.current = true;
|
deepLinkConnectRef.current = true;
|
||||||
void (async () => {
|
if (activeServerId !== deepLink.serverId) {
|
||||||
if (!deepLinkConfigEquals(existing.config, deepLink.serverConfig)) {
|
void onToggleConnection(deepLink.serverId).catch((err) => {
|
||||||
await updateServer(
|
// The toast fires from inside `onToggleConnection` for the common
|
||||||
deepLink.serverId,
|
// cases; this catch covers the rest (surfaced on `data-error-message`).
|
||||||
deepLink.serverId,
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
deepLink.serverConfig,
|
recordConnectError(message);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
if (activeServerId !== deepLink.serverId) {
|
|
||||||
await onToggleConnection(deepLink.serverId);
|
|
||||||
}
|
|
||||||
})().catch((err) => {
|
|
||||||
// Surface deep-link automation failures (updateServer 5xx, connect throw)
|
|
||||||
// on the machine-readable error attribute instead of dropping them. The
|
|
||||||
// toast still fires from inside `onToggleConnection` for the common
|
|
||||||
// cases; this catch covers the rest.
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
recordConnectError(message);
|
|
||||||
});
|
|
||||||
}, [
|
}, [
|
||||||
deepLink,
|
deepLink,
|
||||||
servers,
|
servers,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue