Files
browser/platform_web/schemas/jobs.ts
T

114 lines
5.1 KiB
TypeScript

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"]);
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"),
summary: optionalString(field, "summary"),
content: optionalString(field, "content")
};
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);
}