Files
next-tjwater-frontend/src/features/workbench/api/project-request.ts
T

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;
}
}