stream/sse.ts SseFrameBuffer 去 idx.valueOf()(idx 本就是 number);jobs/job.ts 去 dict["error"] as string(typeof 守卫后 TS 自动收窄)。typecheck 绿。
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
// 长任务(jobs)轮询纯逻辑:把松散 `GET /jobs/{id}` 响应安全收窄成 JobView,
|
||
// 并提供一个 setTimeout 轮询状态机的纯 reducer(C3.5 / ARCH §7.4)。
|
||
// schema 把 GET /jobs/{id} 标成 {[k]:unknown}(T0.3 裸端点),这里安全narrow。
|
||
|
||
// 后端 job 状态机(C3.5):queued → running → done|failed。
|
||
export type JobStatus = "queued" | "running" | "done" | "failed";
|
||
|
||
export interface JobView {
|
||
id: string;
|
||
kind: string;
|
||
status: JobStatus;
|
||
progress: number;
|
||
result: Record<string, unknown> | null;
|
||
error: string | null;
|
||
}
|
||
|
||
function asString(v: unknown, fallback = ""): string {
|
||
return typeof v === "string" ? v : fallback;
|
||
}
|
||
|
||
function asStatus(v: unknown): JobStatus {
|
||
return v === "running" || v === "done" || v === "failed" || v === "queued"
|
||
? v
|
||
: "queued";
|
||
}
|
||
|
||
function asProgress(v: unknown): number {
|
||
if (typeof v !== "number" || !Number.isFinite(v)) return 0;
|
||
return Math.min(100, Math.max(0, Math.round(v)));
|
||
}
|
||
|
||
function asResult(v: unknown): Record<string, unknown> | null {
|
||
return typeof v === "object" && v !== null && !Array.isArray(v)
|
||
? (v as Record<string, unknown>)
|
||
: null;
|
||
}
|
||
|
||
// 把松散 GET /jobs/{id} 响应收窄成 JobView(缺字段给安全默认)。
|
||
export function narrowJob(raw: unknown): JobView {
|
||
const dict =
|
||
typeof raw === "object" && raw !== null ? (raw as Record<string, unknown>) : {};
|
||
return {
|
||
id: asString(dict["id"]),
|
||
kind: asString(dict["kind"]),
|
||
status: asStatus(dict["status"]),
|
||
progress: asProgress(dict["progress"]),
|
||
result: asResult(dict["result"]),
|
||
error: typeof dict["error"] === "string" ? dict["error"] : null,
|
||
};
|
||
}
|
||
|
||
// 学文风 job 完成态的 result:{version, dims_count}(C3 扩 work 摘要)。
|
||
export interface StyleLearnResult {
|
||
version: number | null;
|
||
dimsCount: number | null;
|
||
}
|
||
|
||
export function styleLearnResult(job: JobView): StyleLearnResult {
|
||
const r = job.result ?? {};
|
||
const version = typeof r["version"] === "number" ? r["version"] : null;
|
||
const dimsCount =
|
||
typeof r["dims_count"] === "number" ? r["dims_count"] : null;
|
||
return { version, dimsCount };
|
||
}
|
||
|
||
// 轮询状态机:polling(queued/running)→ done | error(done/failed)。
|
||
export type PollStatus = "polling" | "done" | "error";
|
||
|
||
export interface PollState {
|
||
status: PollStatus;
|
||
progress: number;
|
||
job: JobView | null;
|
||
error: string | null;
|
||
}
|
||
|
||
export const initialPollState: PollState = {
|
||
status: "polling",
|
||
progress: 0,
|
||
job: null,
|
||
error: null,
|
||
};
|
||
|
||
export type PollAction =
|
||
| { type: "tick"; job: JobView }
|
||
| { type: "fail"; error: string };
|
||
|
||
// 纯 reducer:吃一次轮询结果,映射成轮询状态。
|
||
// done → 终态 done;failed → 终态 error(用 job.error 文案);其余 → 继续 polling。
|
||
export function reducePoll(state: PollState, action: PollAction): PollState {
|
||
if (action.type === "fail") {
|
||
return { ...state, status: "error", error: action.error };
|
||
}
|
||
const { job } = action;
|
||
if (job.status === "done") {
|
||
return { status: "done", progress: 100, job, error: null };
|
||
}
|
||
if (job.status === "failed") {
|
||
return {
|
||
status: "error",
|
||
progress: job.progress,
|
||
job,
|
||
error: job.error ?? "任务失败",
|
||
};
|
||
}
|
||
return { status: "polling", progress: job.progress, job, error: null };
|
||
}
|
||
|
||
// 是否终态(停止轮询)。
|
||
export function isTerminal(status: JobStatus): boolean {
|
||
return status === "done" || status === "failed";
|
||
}
|