166 lines
8.0 KiB
TypeScript
166 lines
8.0 KiB
TypeScript
import type { ErrorCode } from "./error";
|
|
|
|
export const LIBRARY_OVERRIDE_FRESHNESS_SCHEMA = 1 as const;
|
|
export const LIBRARY_OVERRIDE_COMMIT_OPERATION = "COMMIT_OVERRIDE" as const;
|
|
|
|
export interface OverrideFreshnessStateIR {
|
|
schemaVersion: typeof LIBRARY_OVERRIDE_FRESHNESS_SCHEMA;
|
|
sourceLibraryId: string;
|
|
sourceGeneration: number;
|
|
sourceRevision: number;
|
|
dependencyClosureSha256: string;
|
|
invalidationToken: string;
|
|
localDataBlockId: string;
|
|
referenceSourceDataBlockId: string;
|
|
hierarchyRootDataBlockId: string;
|
|
owner: "LOCAL_OVERRIDE";
|
|
readOnly: false;
|
|
referenceReadOnly: true;
|
|
}
|
|
|
|
export interface OverrideFreshnessRequestIR {
|
|
schemaVersion: typeof LIBRARY_OVERRIDE_FRESHNESS_SCHEMA;
|
|
operation: typeof LIBRARY_OVERRIDE_COMMIT_OPERATION;
|
|
sourceLibraryId: string;
|
|
sourceGeneration: number;
|
|
sourceRevision: number;
|
|
dependencyClosureSha256: string;
|
|
invalidationToken: string;
|
|
baseRevision: number;
|
|
localDataBlockId: string;
|
|
referenceSourceDataBlockId: string;
|
|
hierarchyRootDataBlockId: string;
|
|
owner: "LOCAL_OVERRIDE";
|
|
readOnly: false;
|
|
referenceReadOnly: true;
|
|
}
|
|
|
|
export interface OverrideFreshnessDecisionIR {
|
|
status: "READY" | "BLOCKED";
|
|
code: ErrorCode | null;
|
|
}
|
|
|
|
export class OverrideFreshnessValidationError extends Error {
|
|
readonly code: ErrorCode;
|
|
readonly path?: string;
|
|
|
|
constructor(code: ErrorCode, message: string, path?: string) {
|
|
super(`${code}: ${message}`);
|
|
this.name = "OverrideFreshnessValidationError";
|
|
this.code = code;
|
|
this.path = path;
|
|
}
|
|
}
|
|
|
|
const LIBRARY_ID = /^library:[a-f0-9]{64}$/;
|
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
const TOKEN = /^override-token:[a-f0-9]{64}$/;
|
|
const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/;
|
|
|
|
function record(value: unknown, path: string): Record<string, unknown> {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path);
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
function exactKeys(value: Record<string, unknown>, expected: readonly string[], path: string): void {
|
|
const actual = Object.keys(value).sort();
|
|
const allowed = [...expected].sort();
|
|
if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path);
|
|
}
|
|
|
|
function integer(value: unknown, path: string): number {
|
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path);
|
|
return value;
|
|
}
|
|
|
|
function id(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path);
|
|
return value;
|
|
}
|
|
|
|
function digest(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !SHA256.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a SHA-256 digest`, path);
|
|
return value;
|
|
}
|
|
|
|
function library(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !LIBRARY_ID.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path);
|
|
return value;
|
|
}
|
|
|
|
function token(value: unknown, path: string): string {
|
|
if (typeof value !== "string" || !TOKEN.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be an invalidation token`, path);
|
|
return value;
|
|
}
|
|
|
|
function ownership(value: Record<string, unknown>, path: string): void {
|
|
if (value.owner !== "LOCAL_OVERRIDE" || value.readOnly !== false || value.referenceReadOnly !== true) throw new OverrideFreshnessValidationError("LINKED_DATA_MUTATION_BLOCKED", `${path} ownership semantics are invalid`, path);
|
|
}
|
|
|
|
const COMMON_KEYS = ["schemaVersion", "sourceLibraryId", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", "invalidationToken", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly"] as const;
|
|
|
|
export function parseOverrideFreshnessState(value: unknown): OverrideFreshnessStateIR {
|
|
const state = record(value, "state");
|
|
exactKeys(state, COMMON_KEYS, "state");
|
|
if (state.schemaVersion !== LIBRARY_OVERRIDE_FRESHNESS_SCHEMA) throw new OverrideFreshnessValidationError("PROTOCOL_MISMATCH", "Unsupported override freshness state schema", "schemaVersion");
|
|
ownership(state, "state");
|
|
const sourceLibraryId = library(state.sourceLibraryId, "sourceLibraryId");
|
|
return {
|
|
schemaVersion: LIBRARY_OVERRIDE_FRESHNESS_SCHEMA,
|
|
sourceLibraryId,
|
|
sourceGeneration: integer(state.sourceGeneration, "sourceGeneration"),
|
|
sourceRevision: integer(state.sourceRevision, "sourceRevision"),
|
|
dependencyClosureSha256: digest(state.dependencyClosureSha256, "dependencyClosureSha256"),
|
|
invalidationToken: token(state.invalidationToken, "invalidationToken"),
|
|
localDataBlockId: id(state.localDataBlockId, "localDataBlockId"),
|
|
referenceSourceDataBlockId: id(state.referenceSourceDataBlockId, "referenceSourceDataBlockId"),
|
|
hierarchyRootDataBlockId: id(state.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"),
|
|
owner: "LOCAL_OVERRIDE",
|
|
readOnly: false,
|
|
referenceReadOnly: true,
|
|
};
|
|
}
|
|
|
|
export function parseOverrideFreshnessRequest(value: unknown): OverrideFreshnessRequestIR {
|
|
const request = record(value, "request");
|
|
exactKeys(request, [...COMMON_KEYS, "operation", "baseRevision"], "request");
|
|
if (request.schemaVersion !== LIBRARY_OVERRIDE_FRESHNESS_SCHEMA) throw new OverrideFreshnessValidationError("PROTOCOL_MISMATCH", "Unsupported override freshness request schema", "schemaVersion");
|
|
if (request.operation !== LIBRARY_OVERRIDE_COMMIT_OPERATION) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", "override commit operation is invalid", "operation");
|
|
ownership(request, "request");
|
|
return {
|
|
schemaVersion: LIBRARY_OVERRIDE_FRESHNESS_SCHEMA,
|
|
operation: LIBRARY_OVERRIDE_COMMIT_OPERATION,
|
|
sourceLibraryId: library(request.sourceLibraryId, "sourceLibraryId"),
|
|
sourceGeneration: integer(request.sourceGeneration, "sourceGeneration"),
|
|
sourceRevision: integer(request.sourceRevision, "sourceRevision"),
|
|
dependencyClosureSha256: digest(request.dependencyClosureSha256, "dependencyClosureSha256"),
|
|
invalidationToken: token(request.invalidationToken, "invalidationToken"),
|
|
baseRevision: integer(request.baseRevision, "baseRevision"),
|
|
localDataBlockId: id(request.localDataBlockId, "localDataBlockId"),
|
|
referenceSourceDataBlockId: id(request.referenceSourceDataBlockId, "referenceSourceDataBlockId"),
|
|
hierarchyRootDataBlockId: id(request.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"),
|
|
owner: "LOCAL_OVERRIDE",
|
|
readOnly: false,
|
|
referenceReadOnly: true,
|
|
};
|
|
}
|
|
|
|
export function gateOverrideFreshness(stateValue: unknown, requestValue: unknown): OverrideFreshnessDecisionIR {
|
|
let state: OverrideFreshnessStateIR;
|
|
let request: OverrideFreshnessRequestIR;
|
|
try {
|
|
state = parseOverrideFreshnessState(stateValue);
|
|
request = parseOverrideFreshnessRequest(requestValue);
|
|
}
|
|
catch (error) {
|
|
return { status: "BLOCKED", code: error instanceof OverrideFreshnessValidationError ? error.code : "TASK_VALIDATION_FAILED" };
|
|
}
|
|
if (request.baseRevision !== state.sourceRevision || request.sourceLibraryId !== state.sourceLibraryId || request.sourceGeneration !== state.sourceGeneration || request.sourceRevision !== state.sourceRevision || request.dependencyClosureSha256 !== state.dependencyClosureSha256 || request.invalidationToken !== state.invalidationToken) {
|
|
return { status: "BLOCKED", code: "REVISION_CONFLICT" };
|
|
}
|
|
if (request.localDataBlockId !== state.localDataBlockId || request.referenceSourceDataBlockId !== state.referenceSourceDataBlockId || request.hierarchyRootDataBlockId !== state.hierarchyRootDataBlockId) {
|
|
return { status: "BLOCKED", code: "ASSET_SOURCE_HASH_MISMATCH" };
|
|
}
|
|
return { status: "READY", code: null };
|
|
}
|