94 lines
3.0 KiB
TypeScript
94 lines
3.0 KiB
TypeScript
import { useCallback, useRef, useState } from "react";
|
|
|
|
import type { JobResponse } from "../api/types";
|
|
import type { OperationRecord, OperationStatus } from "../contracts/workspace";
|
|
|
|
export interface BeginOperationInput {
|
|
intent: string;
|
|
targetKind: OperationRecord["targetKind"];
|
|
targetId: string;
|
|
requester?: string;
|
|
}
|
|
|
|
export interface OperationUpdate {
|
|
status?: OperationStatus;
|
|
job?: JobResponse;
|
|
message?: string;
|
|
errorReason?: string;
|
|
diagnosticId?: string;
|
|
}
|
|
|
|
export interface OperationTracker {
|
|
operations: OperationRecord[];
|
|
begin: (input: BeginOperationInput) => string;
|
|
update: (operationId: string, update: OperationUpdate) => void;
|
|
succeed: (operationId: string, message?: string, job?: JobResponse) => void;
|
|
fail: (operationId: string, errorReason: string, diagnosticId?: string) => void;
|
|
isPending: (targetId: string, intent?: string) => boolean;
|
|
}
|
|
|
|
export function useOperationTracker(): OperationTracker {
|
|
const [operations, setOperations] = useState<OperationRecord[]>([]);
|
|
const sequence = useRef(0);
|
|
|
|
const begin = useCallback((input: BeginOperationInput): string => {
|
|
sequence.current += 1;
|
|
const now = new Date().toISOString();
|
|
const record: OperationRecord = {
|
|
id: `op-${Date.now()}-${sequence.current}`,
|
|
intent: input.intent,
|
|
targetKind: input.targetKind,
|
|
targetId: input.targetId,
|
|
requester: input.requester ?? "当前用户",
|
|
status: "pending",
|
|
createdAt: now,
|
|
updatedAt: now
|
|
};
|
|
setOperations((current) => [record, ...current]);
|
|
return record.id;
|
|
}, []);
|
|
|
|
const update = useCallback((operationId: string, patch: OperationUpdate) => {
|
|
setOperations((current) =>
|
|
current.map((operation) =>
|
|
operation.id === operationId
|
|
? {
|
|
...operation,
|
|
status: patch.status ?? operation.status,
|
|
jobId: patch.job?.id ?? operation.jobId,
|
|
jobState: patch.job?.state ?? operation.jobState,
|
|
message: patch.message ?? operation.message,
|
|
errorReason: patch.errorReason ?? operation.errorReason,
|
|
diagnosticId: patch.diagnosticId ?? operation.diagnosticId,
|
|
updatedAt: new Date().toISOString()
|
|
}
|
|
: operation
|
|
)
|
|
);
|
|
}, []);
|
|
|
|
const succeed = useCallback(
|
|
(operationId: string, message?: string, job?: JobResponse) => {
|
|
update(operationId, { status: "succeeded", message, job });
|
|
},
|
|
[update]
|
|
);
|
|
|
|
const fail = useCallback(
|
|
(operationId: string, errorReason: string, diagnosticId?: string) => {
|
|
update(operationId, { status: "failed", errorReason, diagnosticId: diagnosticId ?? operationId });
|
|
},
|
|
[update]
|
|
);
|
|
|
|
const isPending = useCallback(
|
|
(targetId: string, intent?: string) =>
|
|
operations.some(
|
|
(operation) => operation.status === "pending" && operation.targetId === targetId && (intent === undefined || operation.intent === intent)
|
|
),
|
|
[operations]
|
|
);
|
|
|
|
return { operations, begin, update, succeed, fail, isPending };
|
|
}
|