70 lines
2.8 KiB
TypeScript
70 lines
2.8 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const PROJECT_REQUEST_TIMEOUT_MS = 15_000;
|
|
const READINESS_TIMEOUT_MS = 3_000;
|
|
const readinessSchema = z.object({
|
|
status: z.enum(["ready", "not_ready"]),
|
|
checks: z.object({
|
|
startup: z.string(),
|
|
metadata_database: z.string(),
|
|
mcp_transport: z.string()
|
|
})
|
|
});
|
|
|
|
const isTimeout = (error: unknown) => Boolean(error && typeof error === "object"
|
|
&& "name" in error && error.name === "TimeoutError");
|
|
|
|
async function describeServerReadiness(baseUrl: string): Promise<string> {
|
|
try {
|
|
const response = await fetch(`${baseUrl}/ready`, {
|
|
signal: AbortSignal.timeout(READINESS_TIMEOUT_MS),
|
|
headers: { Accept: "application/json" },
|
|
cache: "no-store"
|
|
});
|
|
const parsed = readinessSchema.safeParse(await response.json());
|
|
if (parsed.success) {
|
|
const { status, checks } = parsed.data;
|
|
if (checks.startup === "not_ready") {
|
|
return "后端服务正在启动,尚未准备好处理项目请求。请稍后重新加载。";
|
|
}
|
|
if (checks.metadata_database === "unavailable") {
|
|
return "后端项目元数据库暂不可用。请稍后重试,若持续失败,请检查后端数据库连接。";
|
|
}
|
|
if (checks.mcp_transport === "not_ready") {
|
|
return "后端查询服务尚未就绪。请稍后重新加载。";
|
|
}
|
|
if (response.ok && status === "ready") {
|
|
return "后端当前已就绪,请重新加载项目。";
|
|
}
|
|
}
|
|
return "暂时无法确认后端就绪状态。服务可能正在重启或响应缓慢,请稍后重新加载。";
|
|
} catch {
|
|
return "后端服务未响应,可能正在重启或热重载。请稍后重新加载,若持续失败,请检查后端服务。";
|
|
}
|
|
}
|
|
|
|
/** Keep caller cancellation distinct from a request deadline or unavailable backend. */
|
|
export async function withProjectRequest<T>(
|
|
baseUrl: string,
|
|
operation: string,
|
|
request: (signal: AbortSignal) => Promise<T>,
|
|
signal?: AbortSignal
|
|
): Promise<T> {
|
|
const deadline = AbortSignal.timeout(PROJECT_REQUEST_TIMEOUT_MS);
|
|
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
try {
|
|
combined.throwIfAborted();
|
|
return await request(combined);
|
|
} catch (error) {
|
|
if (signal?.aborted && !isTimeout(signal.reason)) throw signal.reason;
|
|
if (isTimeout(error) || (combined.aborted && isTimeout(combined.reason))) {
|
|
const diagnosis = await describeServerReadiness(baseUrl);
|
|
throw new Error(`${operation}超时(已等待 15 秒)。${diagnosis}`, { cause: error });
|
|
}
|
|
if (error instanceof TypeError || (error instanceof DOMException && error.name === "NetworkError")) {
|
|
throw new Error(`${operation}失败:无法连接后端服务。请检查网络和后端服务状态后重试。`, { cause: error });
|
|
}
|
|
throw error;
|
|
}
|
|
}
|