Files
workinf_Blender_Wasm/web/protocol/library-override-freshness.ts
mes123456 380cbed4ff
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Checkpoint web parity through Chromium input tasks
2026-08-19 10:39:03 -04:00

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 };
}