feat(web): complete Blender route runtime execution
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ErrorCode } from "./error";
|
||||
import type { BlenderOperatorPropertyValue } from "./blender-operator";
|
||||
import type { BlenderOperatorDescriptor, BlenderOperatorPropertyValue } from "./blender-operator";
|
||||
|
||||
export const SERVER_OPERATOR_JOB_SCHEMA = 1 as const;
|
||||
export const SERVER_OPERATOR_JOB_ROUTE = "SERVER_NATIVE" as const;
|
||||
@@ -14,6 +14,7 @@ export const SERVER_OPERATOR_JOB_BUDGET = {
|
||||
maxTimeoutMs: 10 * 60 * 1000,
|
||||
minMemoryBytes: 64 * 1024 * 1024,
|
||||
maxMemoryBytes: 4 * 1024 * 1024 * 1024,
|
||||
maxEnvelopeBytes: 512 * 1024 * 1024 + 1024 * 1024,
|
||||
} as const;
|
||||
export const SERVER_OPERATOR_JOB_STATUSES = ["QUEUED", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED"] as const;
|
||||
export type ServerOperatorJobStatus = typeof SERVER_OPERATOR_JOB_STATUSES[number];
|
||||
@@ -23,6 +24,13 @@ export interface ServerOperatorBuildIdentityIR {
|
||||
version: string;
|
||||
buildSha256: string;
|
||||
}
|
||||
|
||||
export interface ServerOperatorSchemaResultIR {
|
||||
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
|
||||
route: typeof SERVER_OPERATOR_JOB_ROUTE;
|
||||
operator: BlenderOperatorDescriptor;
|
||||
blenderBuild: ServerOperatorBuildIdentityIR;
|
||||
}
|
||||
export interface ServerOperatorJobRequestIR {
|
||||
schemaVersion: typeof SERVER_OPERATOR_JOB_SCHEMA;
|
||||
route: typeof SERVER_OPERATOR_JOB_ROUTE;
|
||||
@@ -52,11 +60,57 @@ export interface ServerOperatorJobResultIR {
|
||||
propertiesSha256: string;
|
||||
requestSha256: string;
|
||||
resultSha256: string;
|
||||
resultBlendSha256?: string;
|
||||
resultBlendByteLength?: number;
|
||||
resultBlendBase64?: string;
|
||||
result?: unknown;
|
||||
errorCode?: ErrorCode;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const SERVER_OPERATOR_ENVELOPE_MAGIC = "WBJ1";
|
||||
const SERVER_OPERATOR_ENVELOPE_HEADER_BYTES = 16;
|
||||
|
||||
export function canonicalServerOperatorProperties(properties: Record<string, BlenderOperatorPropertyValue>): string {
|
||||
return stableJSON(properties);
|
||||
}
|
||||
|
||||
export function encodeServerOperatorJobEnvelope(request: ServerOperatorJobRequestIR, sourceBlend: ArrayBuffer | Uint8Array): ArrayBuffer {
|
||||
const validated = parseServerOperatorJobRequest(request);
|
||||
const source = sourceBlend instanceof Uint8Array ? sourceBlend : new Uint8Array(sourceBlend);
|
||||
if (source.byteLength !== validated.sourceBlendByteLength) throw new ServerOperatorJobValidationError("source blend byte length does not match request");
|
||||
const requestBytes = new TextEncoder().encode(JSON.stringify(validated));
|
||||
const total = SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength + source.byteLength;
|
||||
if (total > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) throw new ServerOperatorJobValidationError("binary envelope exceeds the budget");
|
||||
const envelope = new Uint8Array(total);
|
||||
envelope.set(new TextEncoder().encode(SERVER_OPERATOR_ENVELOPE_MAGIC), 0);
|
||||
const view = new DataView(envelope.buffer);
|
||||
view.setUint32(4, SERVER_OPERATOR_JOB_SCHEMA, true);
|
||||
view.setUint32(8, requestBytes.byteLength, true);
|
||||
view.setUint32(12, source.byteLength, true);
|
||||
envelope.set(requestBytes, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES);
|
||||
envelope.set(source, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes.byteLength);
|
||||
return envelope.buffer;
|
||||
}
|
||||
|
||||
export function decodeServerOperatorJobEnvelope(value: ArrayBuffer | Uint8Array): { request: ServerOperatorJobRequestIR; sourceBlend: Uint8Array } {
|
||||
const envelope = value instanceof Uint8Array ? value : new Uint8Array(value);
|
||||
if (envelope.byteLength < SERVER_OPERATOR_ENVELOPE_HEADER_BYTES || new TextDecoder().decode(envelope.slice(0, 4)) !== SERVER_OPERATOR_ENVELOPE_MAGIC) {
|
||||
throw new ServerOperatorJobValidationError("binary envelope header is invalid");
|
||||
}
|
||||
const view = new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength);
|
||||
if (view.getUint32(4, true) !== SERVER_OPERATOR_JOB_SCHEMA) throw new ServerOperatorJobValidationError("binary envelope schema is invalid");
|
||||
const requestBytes = view.getUint32(8, true);
|
||||
const sourceBytes = view.getUint32(12, true);
|
||||
if (requestBytes < 1 || sourceBytes < 1 || SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes + sourceBytes !== envelope.byteLength || envelope.byteLength > SERVER_OPERATOR_JOB_BUDGET.maxEnvelopeBytes) {
|
||||
throw new ServerOperatorJobValidationError("binary envelope lengths are invalid");
|
||||
}
|
||||
const request = parseServerOperatorJobRequest(JSON.parse(new TextDecoder().decode(envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES, SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes))));
|
||||
const sourceBlend = envelope.slice(SERVER_OPERATOR_ENVELOPE_HEADER_BYTES + requestBytes);
|
||||
if (sourceBlend.byteLength !== request.sourceBlendByteLength) throw new ServerOperatorJobValidationError("binary envelope source length is invalid");
|
||||
return { request, sourceBlend };
|
||||
}
|
||||
|
||||
export class ServerOperatorJobValidationError extends Error {
|
||||
readonly code: ErrorCode;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user