feat: 完整游戏运维功能
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import type { JobResponse, JobState } from "../api/types";
|
||||
|
||||
const jobStates = new Set<JobState>(["queued", "accepted", "running", "retrying", "succeeded", "failed", "cancelled"]);
|
||||
const forbiddenProjectionKeys = new Set(["leaseToken", "leaseTokenHash", "leaseSessionGeneration", "sessionToken", "secretRef", "hostPath", "socket", "credential", "content"]);
|
||||
|
||||
export function parseSafeJobResponse(value: unknown): JobResponse {
|
||||
if (!isRecord(value)) throw new Error("job projection must be an object");
|
||||
rejectForbiddenKeys(value);
|
||||
const state = requiredString(value, "state") as JobState;
|
||||
if (!jobStates.has(state)) throw new Error("job state is invalid");
|
||||
const progress = requiredRecord(value, "progress");
|
||||
const retryPolicy = requiredRecord(value, "retryPolicy");
|
||||
const parsed: JobResponse = {
|
||||
id: requiredString(value, "id"),
|
||||
serverInstanceId: optionalString(value, "serverInstanceId"),
|
||||
runEndpointId: requiredString(value, "runEndpointId"),
|
||||
capability: requiredString(value, "capability"),
|
||||
targetKey: optionalString(value, "targetKey"),
|
||||
inputRef: optionalString(value, "inputRef"),
|
||||
idempotencyKey: requiredString(value, "idempotencyKey"),
|
||||
state,
|
||||
progress: { percent: requiredNumber(progress, "percent"), message: optionalString(progress, "message") },
|
||||
resultRef: optionalString(value, "resultRef"),
|
||||
executionResult: optionalExecutionResult(value, "executionResult"),
|
||||
retryPolicy: {
|
||||
maxAttempts: requiredNumber(retryPolicy, "maxAttempts"),
|
||||
initialBackoffSeconds: requiredNumber(retryPolicy, "initialBackoffSeconds"),
|
||||
maxBackoffSeconds: requiredNumber(retryPolicy, "maxBackoffSeconds")
|
||||
},
|
||||
attempt: requiredNumber(value, "attempt"),
|
||||
nextAttemptAt: optionalString(value, "nextAttemptAt"),
|
||||
ackDeadlineAt: optionalString(value, "ackDeadlineAt"),
|
||||
leaseExpiresAt: optionalString(value, "leaseExpiresAt"),
|
||||
cancelReason: optionalString(value, "cancelReason"),
|
||||
cancelRequestedAt: optionalString(value, "cancelRequestedAt"),
|
||||
cancelCompletedAt: optionalString(value, "cancelCompletedAt"),
|
||||
terminalAt: optionalString(value, "terminalAt"),
|
||||
lastReconciledAt: optionalString(value, "lastReconciledAt"),
|
||||
reconcileCount: requiredNumber(value, "reconcileCount"),
|
||||
reconcileOutcome: optionalString(value, "reconcileOutcome"),
|
||||
createdAt: requiredString(value, "createdAt"),
|
||||
updatedAt: requiredString(value, "updatedAt")
|
||||
};
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function optionalExecutionResult(value: Record<string, unknown>, key: string): JobResponse["executionResult"] {
|
||||
const field = value[key];
|
||||
if (field === undefined) return undefined;
|
||||
if (!isRecord(field)) throw new Error(`${key} must be an object`);
|
||||
rejectForbiddenKeys(field);
|
||||
const result: NonNullable<JobResponse["executionResult"]> = {
|
||||
kind: optionalString(field, "kind"),
|
||||
processState: optionalString(field, "processState"),
|
||||
exitClassification: optionalString(field, "exitClassification"),
|
||||
exitCode: optionalSignedNumber(field, "exitCode"),
|
||||
version: optionalNumber(field, "version"),
|
||||
checksum: optionalString(field, "checksum"),
|
||||
sizeBytes: optionalNumber(field, "sizeBytes"),
|
||||
auditSummary: optionalString(field, "auditSummary")
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
function rejectForbiddenKeys(value: Record<string, unknown>): void {
|
||||
for (const key of Object.keys(value)) {
|
||||
if (forbiddenProjectionKeys.has(key)) throw new Error(`job projection contains forbidden field ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requiredRecord(value: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const field = value[key];
|
||||
if (!isRecord(field)) throw new Error(`${key} must be an object`);
|
||||
rejectForbiddenKeys(field);
|
||||
return field;
|
||||
}
|
||||
|
||||
function requiredString(value: Record<string, unknown>, key: string): string {
|
||||
const field = value[key];
|
||||
if (typeof field !== "string" || field.trim() === "") throw new Error(`${key} must be a non-empty string`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalString(value: Record<string, unknown>, key: string): string | undefined {
|
||||
const field = value[key];
|
||||
if (field === undefined) return undefined;
|
||||
if (typeof field !== "string") throw new Error(`${key} must be a string`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalSignedNumber(value: Record<string, unknown>, key: string): number | undefined {
|
||||
const field = value[key];
|
||||
if (field === undefined) return undefined;
|
||||
if (typeof field !== "number" || !Number.isFinite(field)) throw new Error(`${key} must be a number`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function optionalNumber(value: Record<string, unknown>, key: string): number | undefined {
|
||||
const field = optionalSignedNumber(value, key);
|
||||
if (field !== undefined && field < 0) throw new Error(`${key} must be non-negative`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function requiredNumber(value: Record<string, unknown>, key: string): number {
|
||||
const field = value[key];
|
||||
if (typeof field !== "number" || !Number.isFinite(field) || field < 0) throw new Error(`${key} must be a non-negative number`);
|
||||
return field;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user