Files
workinf_Blender_Wasm/web/protocol/library-linked-missing.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

229 lines
11 KiB
TypeScript

import type { ErrorCode } from "./error";
export const LIBRARY_LINKED_MISSING_SCHEMA = 1 as const;
export const LINKED_MISSING_OPERATION = "MARK_MISSING" as const;
export interface MissingLibraryPlaceholderIR {
kind: "MISSING_LIBRARY";
sourceLibraryId: string;
dataBlockIds: string[];
}
export interface LinkedLibraryReferenceIR {
sourceLibraryId: string;
sourceLocator: string;
sourceSha256: string;
sourceGeneration: number;
sourceRevision: number;
dataBlockIds: string[];
status: "AVAILABLE" | "MISSING";
placeholder: MissingLibraryPlaceholderIR | null;
}
export interface LinkedMissingStateIR {
schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA;
references: LinkedLibraryReferenceIR[];
}
export interface LinkedMissingRequestIR {
schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA;
operation: typeof LINKED_MISSING_OPERATION;
sourceLibraryId: string;
sourceLocator: string;
sourceSha256: string;
expectedGeneration: number;
expectedRevision: number;
}
export interface LinkedMissingDecisionIR {
status: "MARKED" | "STALE";
code: ErrorCode | null;
sourceLibraryId: string;
state: LinkedMissingStateIR;
}
export class LinkedMissingValidationError extends Error {
readonly code: ErrorCode;
readonly path?: string;
constructor(code: ErrorCode, message: string, path?: string) {
super(`${code}: ${message}`);
this.name = "LinkedMissingValidationError";
this.code = code;
this.path = path;
}
}
const LIBRARY_ID = /^library:[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const SOURCE_LOCATOR = /^[^\u0000\r\n]{1,4096}$/;
const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/;
const MAX_REFERENCES = 10_000;
const MAX_DATA_BLOCKS = 256;
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new LinkedMissingValidationError("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 LinkedMissingValidationError("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 LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path);
}
return value;
}
function libraryId(value: unknown, path: string): string {
if (typeof value !== "string" || !LIBRARY_ID.test(value)) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path);
}
return value;
}
function digest(value: unknown, path: string): string {
if (typeof value !== "string" || !SHA256.test(value)) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a lowercase SHA-256 digest`, path);
}
return value;
}
function locator(value: unknown, path: string): string {
if (typeof value !== "string" || !SOURCE_LOCATOR.test(value)) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside the source locator budget`, path);
}
return value;
}
function dataBlockIds(value: unknown, path: string): string[] {
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_DATA_BLOCKS ||
value.some((item) => typeof item !== "string" || !DATA_BLOCK_ID.test(item))) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside its bounded ID list`, path);
}
const result = [...value] as string[];
if (new Set(result).size !== result.length) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} contains duplicate IDs`, path);
}
return result;
}
function parsePlaceholder(value: unknown, path: string): MissingLibraryPlaceholderIR | null {
if (value === null) return null;
const placeholder = record(value, path);
exactKeys(placeholder, ["kind", "sourceLibraryId", "dataBlockIds"], path);
if (placeholder.kind !== "MISSING_LIBRARY") {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.kind is invalid`, `${path}.kind`);
}
return {
kind: "MISSING_LIBRARY",
sourceLibraryId: libraryId(placeholder.sourceLibraryId, `${path}.sourceLibraryId`),
dataBlockIds: dataBlockIds(placeholder.dataBlockIds, `${path}.dataBlockIds`),
};
}
export function parseLinkedLibraryReference(value: unknown, path = "reference"): LinkedLibraryReferenceIR {
const reference = record(value, path);
exactKeys(reference, ["sourceLibraryId", "sourceLocator", "sourceSha256", "sourceGeneration", "sourceRevision", "dataBlockIds", "status", "placeholder"], path);
const sourceLibraryId = libraryId(reference.sourceLibraryId, `${path}.sourceLibraryId`);
const sourceLocator = locator(reference.sourceLocator, `${path}.sourceLocator`);
const sourceSha256 = digest(reference.sourceSha256, `${path}.sourceSha256`);
const sourceGeneration = integer(reference.sourceGeneration, `${path}.sourceGeneration`);
const sourceRevision = integer(reference.sourceRevision, `${path}.sourceRevision`);
const ids = dataBlockIds(reference.dataBlockIds, `${path}.dataBlockIds`);
if (reference.status !== "AVAILABLE" && reference.status !== "MISSING") {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.status is invalid`, `${path}.status`);
}
const placeholder = parsePlaceholder(reference.placeholder, `${path}.placeholder`);
if (reference.status === "AVAILABLE" && placeholder !== null) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} available reference cannot have a placeholder`, path);
}
if (reference.status === "MISSING" && (placeholder === null || placeholder.sourceLibraryId !== sourceLibraryId ||
placeholder.dataBlockIds.length !== ids.length || placeholder.dataBlockIds.some((id, index) => id !== ids[index]))) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} missing placeholder must preserve the source IDs`, path);
}
return { sourceLibraryId, sourceLocator, sourceSha256, sourceGeneration, sourceRevision, dataBlockIds: ids, status: reference.status, placeholder };
}
export function parseLinkedMissingState(value: unknown): LinkedMissingStateIR {
const state = record(value, "state");
exactKeys(state, ["schemaVersion", "references"], "state");
if (state.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) {
throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library state schema", "schemaVersion");
}
if (!Array.isArray(state.references) || state.references.length > MAX_REFERENCES) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state.references exceeds its bounded range", "references");
}
const references = state.references.map((item, index) => parseLinkedLibraryReference(item, `state.references[${index}]`));
const identities = references.map((item) => `${item.sourceLibraryId}:${item.sourceGeneration}`);
if (new Set(identities).size !== identities.length) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state contains duplicate source generations", "references");
}
return { schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, references };
}
export function parseLinkedMissingRequest(value: unknown): LinkedMissingRequestIR {
const request = record(value, "request");
exactKeys(request, ["schemaVersion", "operation", "sourceLibraryId", "sourceLocator", "sourceSha256", "expectedGeneration", "expectedRevision"], "request");
if (request.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) {
throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library request schema", "schemaVersion");
}
if (request.operation !== LINKED_MISSING_OPERATION) {
throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "missing-library operation is invalid", "operation");
}
return {
schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA,
operation: LINKED_MISSING_OPERATION,
sourceLibraryId: libraryId(request.sourceLibraryId, "request.sourceLibraryId"),
sourceLocator: locator(request.sourceLocator, "request.sourceLocator"),
sourceSha256: digest(request.sourceSha256, "request.sourceSha256"),
expectedGeneration: integer(request.expectedGeneration, "request.expectedGeneration"),
expectedRevision: integer(request.expectedRevision, "request.expectedRevision"),
};
}
function cloneState(state: LinkedMissingStateIR): LinkedMissingStateIR {
return {
schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA,
references: state.references.map((reference) => ({
...reference,
dataBlockIds: [...reference.dataBlockIds],
placeholder: reference.placeholder === null ? null : {
...reference.placeholder,
dataBlockIds: [...reference.placeholder.dataBlockIds],
},
})),
};
}
export function markLinkedLibraryMissing(stateValue: unknown, requestValue: unknown): LinkedMissingDecisionIR {
const state = parseLinkedMissingState(stateValue);
const request = parseLinkedMissingRequest(requestValue);
const index = state.references.findIndex((reference) =>
reference.sourceLibraryId === request.sourceLibraryId && reference.sourceGeneration === request.expectedGeneration,
);
if (index === -1 || state.references[index].sourceRevision !== request.expectedRevision) {
return { status: "STALE", code: "REVISION_CONFLICT", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) };
}
const current = state.references[index];
if (current.sourceLocator !== request.sourceLocator || current.sourceSha256 !== request.sourceSha256) {
return { status: "STALE", code: "ASSET_SOURCE_HASH_MISMATCH", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) };
}
const missing: LinkedLibraryReferenceIR = {
...current,
status: "MISSING",
placeholder: { kind: "MISSING_LIBRARY", sourceLibraryId: current.sourceLibraryId, dataBlockIds: [...current.dataBlockIds] },
dataBlockIds: [...current.dataBlockIds],
};
const references = state.references.map((reference, itemIndex) => itemIndex === index ? missing : reference);
return { status: "MARKED", code: null, sourceLibraryId: request.sourceLibraryId, state: { schemaVersion: 1, references: references.map((reference) => ({ ...reference, dataBlockIds: [...reference.dataBlockIds], placeholder: reference.placeholder === null ? null : { ...reference.placeholder, dataBlockIds: [...reference.placeholder.dataBlockIds] } })) } };
}