Files
Jyotisha/frontend/src/lib/rectification-v4/client.ts
T

161 lines
5.7 KiB
TypeScript

import { z } from "zod";
import {
rectificationV4ApiResponseSchema,
rectificationV4HandoffSchema,
rectificationV4JobSchema,
type RectificationV4ApiResponse,
type RectificationV4Handoff,
type RectificationV4Job,
} from "./contracts";
const errorSchema = z.object({
error: z.string().optional(),
message: z.string().optional(),
}).passthrough();
function errorMessage(payload: unknown, fallback: string): string {
const parsed = errorSchema.safeParse(payload);
return parsed.success ? parsed.data.message || parsed.data.error || fallback : fallback;
}
export class RectificationV4RequestError extends Error {
constructor(readonly status: number, message: string) {
super(message);
}
}
async function json<T>(response: Response, schema: z.ZodType<T>): Promise<T> {
const payload = await response.json().catch(() => null);
if (!response.ok) {
throw new RectificationV4RequestError(
response.status,
errorMessage(payload, "暂时无法处理,请稍后再试。"),
);
}
return schema.parse(payload);
}
function post(path: string, body: unknown): Promise<RectificationV4ApiResponse> {
return fetch(path, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}).then((response) => json(response, rectificationV4ApiResponseSchema));
}
export async function loadActiveRectificationV4(): Promise<RectificationV4ApiResponse | null> {
const response = await fetch("/api/rectification/v4/cases/active", { cache: "no-store" });
if (response.status === 204) return null;
return json(response, rectificationV4ApiResponseSchema);
}
export function createRectificationV4(): Promise<RectificationV4ApiResponse> {
return post("/api/rectification/v4/cases", { actionId: globalThis.crypto.randomUUID() });
}
export async function loadRectificationV4(caseId: string): Promise<RectificationV4ApiResponse> {
return json(await fetch(`/api/rectification/v4/cases/${caseId}`, { cache: "no-store" }), rectificationV4ApiResponseSchema);
}
export function answerRectificationV4(caseId: string, expectedCaseVersion: number, answer: string) {
return post(`/api/rectification/v4/cases/${caseId}/answers`, {
actionId: globalThis.crypto.randomUUID(), expectedCaseVersion, answer,
});
}
export function transitionRectificationV4(
caseId: string,
expectedCaseVersion: number,
action: "pause" | "resume" | "abandon",
) {
return post(`/api/rectification/v4/cases/${caseId}/${action}`, {
actionId: globalThis.crypto.randomUUID(), expectedCaseVersion,
});
}
export function acceptRectificationV4Range(
caseId: string,
expectedCaseVersion: number,
startTime: string,
endTime: string,
) {
return post(`/api/rectification/v4/cases/${caseId}/accept-range`, {
actionId: globalThis.crypto.randomUUID(), expectedCaseVersion, startTime, endTime,
});
}
export async function loadRectificationV4Job(jobId: string): Promise<RectificationV4Job> {
const schema = z.object({ job: rectificationV4JobSchema }).strict();
return (await json(await fetch(`/api/rectification/v4/jobs/${jobId}`, { cache: "no-store" }), schema)).job;
}
const handoffClaimActions = new Map<string, string>();
async function handoffPayload(response: Response): Promise<unknown> {
if (response.status === 204) return null;
return response.json().catch(() => null);
}
async function handoffPost(body: Readonly<Record<string, unknown>>): Promise<RectificationV4Handoff> {
const serialized = JSON.stringify(body);
let lastError: unknown;
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
const response = await fetch("/api/rectification/v4/handoff", {
method: "POST",
headers: { "content-type": "application/json" },
body: serialized,
});
const payload = await handoffPayload(response);
if (!response.ok) {
throw new RectificationV4RequestError(
response.status,
errorMessage(payload, "暂时无法保存或继续原问题,请稍后重试。"),
);
}
return rectificationV4HandoffSchema.parse(payload);
} catch (error) {
lastError = error;
if (error instanceof RectificationV4RequestError || attempt > 0) throw error;
}
}
throw lastError;
}
export async function loadRectificationV4Handoff(caseId?: string): Promise<RectificationV4Handoff | null> {
const query = caseId ? `?caseId=${encodeURIComponent(caseId)}` : "";
const response = await fetch(`/api/rectification/v4/handoff${query}`, { cache: "no-store" });
const payload = await handoffPayload(response);
if (response.status === 204) return null;
if (!response.ok) {
throw new RectificationV4RequestError(
response.status,
errorMessage(payload, "暂时无法读取原问题,请稍后再试。"),
);
}
return rectificationV4HandoffSchema.parse(payload);
}
export function attachRectificationV4Question(input: Readonly<{
caseId: string;
caseVersion: number;
question: string;
actionId: string;
}>): Promise<RectificationV4Handoff> {
return handoffPost({ type: "attach", ...input, question: input.question.trim() });
}
export async function claimRectificationV4Handoff(input: Readonly<{
caseId: string;
caseVersion: number;
question: string;
}>): Promise<RectificationV4Handoff & Readonly<{ claimActionId: string }>> {
const identity = JSON.stringify([input.caseId, input.caseVersion, input.question.trim()]);
const actionId = handoffClaimActions.get(identity) ?? globalThis.crypto.randomUUID();
handoffClaimActions.set(identity, actionId);
const result = await handoffPost({ type: "claim", ...input, actionId, question: input.question.trim() });
if (result.status !== "claimed") handoffClaimActions.delete(identity);
return Object.freeze({ ...result, claimActionId: actionId });
}