Files
workinf_Blender_Wasm/web/protocol/library-main-append.ts
mes123456 5a11045ca5
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
Advance Blender 5.2 web parity through M12-03D
2026-08-17 17:30:27 -04:00

174 lines
6.7 KiB
TypeScript

import type { ErrorCode } from "./error";
import {
parseLibraryOperationBinding,
type LibraryOperationBindingIR,
} from "./library-operation-identity";
export const LIBRARY_MAIN_APPEND_SCHEMA = 1 as const;
export const LIBRARY_MAIN_APPEND_MAX_SOURCE_BYTES = 64 * 1024 * 1024;
export interface LibraryAppendClosureIR {
object: string;
mesh: string;
material: string;
image: string;
}
export interface LibraryMainAppendRequestIR {
schemaVersion: typeof LIBRARY_MAIN_APPEND_SCHEMA;
baseRevision: number;
binding: LibraryOperationBindingIR;
expectedClosure: LibraryAppendClosureIR;
}
export interface LibraryMainAppendMappingIR {
source: string;
local: string;
owner: "LOCAL_MAIN";
readOnly: false;
}
export interface LibraryMainAppendReceiptIR {
schemaVersion: typeof LIBRARY_MAIN_APPEND_SCHEMA;
operation: "APPEND";
sourceLibraryId: string;
sourceDataBlockId: string;
dependencyClosureSha256: string;
baseRevision: number;
nextRevision: number;
transactionCount: 1;
mapping: LibraryMainAppendMappingIR[];
}
export class LibraryMainAppendError extends Error {
readonly code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "LibraryMainAppendError";
this.code = code;
}
}
const encoder = new TextEncoder();
const PREFIXES = Object.freeze({
object: "Object/",
mesh: "Mesh/",
material: "Material/",
image: "Image/",
});
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function exactKeys(value: Record<string, unknown>, keys: readonly string[], path: string): void {
const allowed = new Set(keys);
if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !allowed.has(key))) {
throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", `${path} fields are not exact`);
}
}
function stableJSON(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`;
if (record(value)) {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
async function sha256Text(value: string): Promise<string> {
const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value));
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
export async function sha256LibraryBytes(value: ArrayBuffer): Promise<string> {
if (!(value instanceof ArrayBuffer) || value.byteLength === 0 || value.byteLength > LIBRARY_MAIN_APPEND_MAX_SOURCE_BYTES) {
throw new LibraryMainAppendError("ASSET_BUDGET_EXCEEDED", "library source must contain 1..64 MiB");
}
const digest = await crypto.subtle.digest("SHA-256", value);
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
}
export function parseLibraryAppendClosure(value: unknown): LibraryAppendClosureIR {
if (!record(value)) throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "expectedClosure must be an object");
exactKeys(value, Object.keys(PREFIXES), "expectedClosure");
const result = {} as Record<keyof LibraryAppendClosureIR, string>;
for (const key of Object.keys(PREFIXES) as Array<keyof LibraryAppendClosureIR>) {
const prefix = PREFIXES[key];
const item = value[key];
if (typeof item !== "string" || !item.startsWith(prefix) || item.length === prefix.length ||
encoder.encode(item.slice(prefix.length)).byteLength > 63) {
throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", `expectedClosure.${key} is invalid`);
}
result[key] = item;
}
if (new Set(Object.values(result)).size !== 4) {
throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "append closure IDs must be unique");
}
return result;
}
export async function computeLibraryAppendClosureSha256(value: unknown): Promise<string> {
const closure = parseLibraryAppendClosure(value);
return sha256Text(stableJSON({ schema: "BLENDER_APPEND_OBJECT_CLOSURE_V1", ...closure }));
}
export async function parseLibraryMainAppendRequest(
value: unknown,
currentRevision?: number,
): Promise<LibraryMainAppendRequestIR> {
if (!record(value) || value.schemaVersion !== LIBRARY_MAIN_APPEND_SCHEMA) {
throw new LibraryMainAppendError("PROTOCOL_MISMATCH", "unsupported library Main append schema");
}
exactKeys(value, ["schemaVersion", "baseRevision", "binding", "expectedClosure"], "request");
if (typeof value.baseRevision !== "number" || !Number.isSafeInteger(value.baseRevision) || value.baseRevision < 0) {
throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "baseRevision must be a non-negative safe integer");
}
if (currentRevision !== undefined && value.baseRevision !== currentRevision) {
throw new LibraryMainAppendError("REVISION_CONFLICT", "library append base revision is stale");
}
const binding = await parseLibraryOperationBinding(value.binding);
const expectedClosure = parseLibraryAppendClosure(value.expectedClosure);
if (binding.operation !== "APPEND" || binding.owner.kind !== "LOCAL_MAIN" || binding.readOnly || binding.referenceReadOnly) {
throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "library Main append requires a writable LOCAL_MAIN binding");
}
if (binding.sourceDataBlockId !== expectedClosure.object || binding.owner.localDataBlockId !== expectedClosure.object) {
throw new LibraryMainAppendError("ASSET_SOURCE_HASH_MISMATCH", "append root and local owner do not match the expected Object");
}
if (binding.dependencyClosureSha256 !== await computeLibraryAppendClosureSha256(expectedClosure)) {
throw new LibraryMainAppendError("REVISION_CONFLICT", "append dependency closure binding is stale");
}
return {
schemaVersion: LIBRARY_MAIN_APPEND_SCHEMA,
baseRevision: value.baseRevision,
binding,
expectedClosure,
};
}
export function createLibraryMainAppendReceipt(
request: LibraryMainAppendRequestIR,
nextRevision: number,
): LibraryMainAppendReceiptIR {
if (!Number.isSafeInteger(nextRevision) || nextRevision !== request.baseRevision + 1) {
throw new LibraryMainAppendError("REVISION_CONFLICT", "library append must advance exactly one Main revision");
}
return {
schemaVersion: LIBRARY_MAIN_APPEND_SCHEMA,
operation: "APPEND",
sourceLibraryId: request.binding.source.sourceLibraryId,
sourceDataBlockId: request.binding.sourceDataBlockId,
dependencyClosureSha256: request.binding.dependencyClosureSha256,
baseRevision: request.baseRevision,
nextRevision,
transactionCount: 1,
mapping: Object.values(request.expectedClosure).map((source) => ({
source,
local: source,
owner: "LOCAL_MAIN",
readOnly: false,
})),
};
}