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

223 lines
9.5 KiB
TypeScript

import type { ErrorCode } from "./error";
export const LIBRARY_LINKED_RELOAD_SCHEMA = 1 as const;
export const LINKED_RELOAD_OPERATION = "RELOAD" as const;
export interface LinkedSnapshotDataBlockIR {
dataBlockId: string;
owner: "SOURCE_LIBRARY";
readOnly: true;
}
export interface LinkedSnapshotIR {
sourceLibraryId: string;
sourceGeneration: number;
sourceRevision: number;
dependencyClosureSha256: string;
graphSha256: string;
dataBlocks: LinkedSnapshotDataBlockIR[];
}
export interface LinkedReloadStateIR {
schemaVersion: typeof LIBRARY_LINKED_RELOAD_SCHEMA;
snapshots: LinkedSnapshotIR[];
}
export interface LinkedReloadRequestIR {
schemaVersion: typeof LIBRARY_LINKED_RELOAD_SCHEMA;
operation: typeof LINKED_RELOAD_OPERATION;
sourceLibraryId: string;
expectedGeneration: number;
expectedRevision: number;
replacement: LinkedSnapshotIR;
}
export interface LinkedReloadDecisionIR {
status: "REPLACED" | "STALE";
code: ErrorCode | null;
sourceLibraryId: string;
replacedGeneration: number;
replacementGeneration: number;
state: LinkedReloadStateIR;
}
export class LinkedReloadValidationError extends Error {
readonly code: ErrorCode;
readonly path?: string;
constructor(code: ErrorCode, message: string, path?: string) {
super(`${code}: ${message}`);
this.name = "LinkedReloadValidationError";
this.code = code;
this.path = path;
}
}
const LIBRARY_ID = /^library:[a-f0-9]{64}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/;
const MAX_SNAPSHOTS = 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 LinkedReloadValidationError("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 keys = Object.keys(value).sort();
const allowed = [...expected].sort();
if (keys.length !== allowed.length || keys.some((key, index) => key !== allowed[index])) {
throw new LinkedReloadValidationError("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 LinkedReloadValidationError("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 LinkedReloadValidationError("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 LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} must be a lowercase SHA-256 digest`, path);
}
return value;
}
function dataBlock(value: unknown, path: string): LinkedSnapshotDataBlockIR {
const item = record(value, path);
exactKeys(item, ["dataBlockId", "owner", "readOnly"], path);
if (typeof item.dataBlockId !== "string" || !DATA_BLOCK_ID.test(item.dataBlockId)) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlockId is invalid`, `${path}.dataBlockId`);
}
if (item.owner !== "SOURCE_LIBRARY" || item.readOnly !== true) {
throw new LinkedReloadValidationError("LINKED_DATA_MUTATION_BLOCKED", `${path} must remain source-library/read-only`, path);
}
return { dataBlockId: item.dataBlockId, owner: "SOURCE_LIBRARY", readOnly: true };
}
export function parseLinkedSnapshot(value: unknown, path = "snapshot"): LinkedSnapshotIR {
const snapshot = record(value, path);
exactKeys(snapshot, ["sourceLibraryId", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", "graphSha256", "dataBlocks"], path);
if (!Array.isArray(snapshot.dataBlocks) || snapshot.dataBlocks.length === 0 || snapshot.dataBlocks.length > MAX_DATA_BLOCKS) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlocks is outside its bounded range`, `${path}.dataBlocks`);
}
const dataBlocks = snapshot.dataBlocks.map((item, index) => dataBlock(item, `${path}.dataBlocks[${index}]`));
if (new Set(dataBlocks.map((item) => item.dataBlockId)).size !== dataBlocks.length) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlocks contains duplicate IDs`, `${path}.dataBlocks`);
}
return {
sourceLibraryId: libraryId(snapshot.sourceLibraryId, `${path}.sourceLibraryId`),
sourceGeneration: integer(snapshot.sourceGeneration, `${path}.sourceGeneration`),
sourceRevision: integer(snapshot.sourceRevision, `${path}.sourceRevision`),
dependencyClosureSha256: digest(snapshot.dependencyClosureSha256, `${path}.dependencyClosureSha256`),
graphSha256: digest(snapshot.graphSha256, `${path}.graphSha256`),
dataBlocks,
};
}
export function parseLinkedReloadState(value: unknown): LinkedReloadStateIR {
const state = record(value, "state");
exactKeys(state, ["schemaVersion", "snapshots"], "state");
if (state.schemaVersion !== LIBRARY_LINKED_RELOAD_SCHEMA) {
throw new LinkedReloadValidationError("PROTOCOL_MISMATCH", "Unsupported linked reload state schema", "schemaVersion");
}
if (!Array.isArray(state.snapshots) || state.snapshots.length > MAX_SNAPSHOTS) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "state.snapshots exceeds its bounded range", "snapshots");
}
const snapshots = state.snapshots.map((item, index) => parseLinkedSnapshot(item, `state.snapshots[${index}]`));
const identities = snapshots.map((item) => `${item.sourceLibraryId}:${item.sourceGeneration}`);
if (new Set(identities).size !== identities.length) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "state contains duplicate library generations", "snapshots");
}
return { schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, snapshots };
}
export function parseLinkedReloadRequest(value: unknown): LinkedReloadRequestIR {
const request = record(value, "request");
exactKeys(request, ["schemaVersion", "operation", "sourceLibraryId", "expectedGeneration", "expectedRevision", "replacement"], "request");
if (request.schemaVersion !== LIBRARY_LINKED_RELOAD_SCHEMA) {
throw new LinkedReloadValidationError("PROTOCOL_MISMATCH", "Unsupported linked reload request schema", "schemaVersion");
}
if (request.operation !== LINKED_RELOAD_OPERATION) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "linked reload operation is invalid", "operation");
}
const replacement = parseLinkedSnapshot(request.replacement, "request.replacement");
const sourceLibraryId = libraryId(request.sourceLibraryId, "request.sourceLibraryId");
if (replacement.sourceLibraryId !== sourceLibraryId) {
throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "replacement must retain the requested source library", "replacement.sourceLibraryId");
}
return {
schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA,
operation: LINKED_RELOAD_OPERATION,
sourceLibraryId,
expectedGeneration: integer(request.expectedGeneration, "request.expectedGeneration"),
expectedRevision: integer(request.expectedRevision, "request.expectedRevision"),
replacement,
};
}
function cloneState(state: LinkedReloadStateIR): LinkedReloadStateIR {
return {
schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA,
snapshots: state.snapshots.map((snapshot) => ({
...snapshot,
dataBlocks: snapshot.dataBlocks.map((dataBlock) => ({ ...dataBlock })),
})),
};
}
export function reloadMatchingLinkedSnapshot(stateValue: unknown, requestValue: unknown): LinkedReloadDecisionIR {
const state = parseLinkedReloadState(stateValue);
const request = parseLinkedReloadRequest(requestValue);
const matchingIndex = state.snapshots.findIndex((snapshot) =>
snapshot.sourceLibraryId === request.sourceLibraryId && snapshot.sourceGeneration === request.expectedGeneration,
);
if (matchingIndex === -1 || state.snapshots[matchingIndex].sourceRevision !== request.expectedRevision) {
return {
status: "STALE",
code: "REVISION_CONFLICT",
sourceLibraryId: request.sourceLibraryId,
replacedGeneration: request.expectedGeneration,
replacementGeneration: request.replacement.sourceGeneration,
state: cloneState(state),
};
}
const current = state.snapshots[matchingIndex];
if (request.replacement.sourceLibraryId !== request.sourceLibraryId ||
request.replacement.sourceGeneration !== request.expectedGeneration + 1 ||
request.replacement.sourceRevision <= current.sourceRevision) {
return {
status: "STALE",
code: "REVISION_CONFLICT",
sourceLibraryId: request.sourceLibraryId,
replacedGeneration: request.expectedGeneration,
replacementGeneration: request.replacement.sourceGeneration,
state: cloneState(state),
};
}
const snapshots = state.snapshots.map((snapshot, index) => index === matchingIndex ? request.replacement : snapshot);
return {
status: "REPLACED",
code: null,
sourceLibraryId: request.sourceLibraryId,
replacedGeneration: current.sourceGeneration,
replacementGeneration: request.replacement.sourceGeneration,
state: { schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, snapshots: snapshots.map((snapshot) => ({
...snapshot,
dataBlocks: snapshot.dataBlocks.map((dataBlock) => ({ ...dataBlock })),
})) },
};
}