Advance M7 workflows and release operations
This commit is contained in:
@@ -199,12 +199,12 @@ async function ensureProject(projectId: string): Promise<StorageProjectResult> {
|
||||
};
|
||||
}
|
||||
|
||||
async function saveProject(projectId: string, revision: number, buffer: ArrayBuffer, faultAt?: ProjectSaveFault | "quota"): Promise<StorageSaveResult> {
|
||||
async function saveProject(projectId: string, revision: number, buffer: ArrayBuffer, faultAt?: ProjectSaveFault | "before-metadata-commit" | "quota"): Promise<StorageSaveResult> {
|
||||
if (faultAt === "quota") throw new Error("QuotaExceededError: injected OPFS quota exhaustion");
|
||||
const useOpfs = await useOpfsForProject(projectId);
|
||||
const layout = useOpfs ? await ensureProjectLayout(projectId) : projectLayout(projectId);
|
||||
const sha256 = await sha256Hex(buffer);
|
||||
const existing = faultAt ? undefined : await readProjectRow(projectId);
|
||||
const existing = await readProjectRow(projectId);
|
||||
if (existing && existing.revision > revision) {
|
||||
throw new Error(`PROJECT_REVISION_CONFLICT: committed revision ${existing.revision} is newer than ${revision}`);
|
||||
}
|
||||
@@ -232,8 +232,24 @@ async function saveProject(projectId: string, revision: number, buffer: ArrayBuf
|
||||
};
|
||||
}
|
||||
if (useOpfs) {
|
||||
const committed = await writeProjectBlend(projectId, revision, buffer, undefined, faultAt);
|
||||
if (committed.manifest.sha256 !== sha256) throw new Error("Project commit digest mismatch");
|
||||
const previousBuffer = faultAt && existing ? await readProjectBlend(projectId) : undefined;
|
||||
try {
|
||||
const opfsFault = faultAt === "after-stage" || faultAt === "after-scene-commit" ? faultAt : undefined;
|
||||
const committed = await writeProjectBlend(projectId, revision, buffer, undefined, opfsFault);
|
||||
if (committed.manifest.sha256 !== sha256) throw new Error("Project commit digest mismatch");
|
||||
if (faultAt === "before-metadata-commit") {
|
||||
throw new Error("PROJECT_SAVE_FAULT_INJECTED: before-metadata-commit");
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (faultAt && existing && previousBuffer) {
|
||||
const rollback = await writeProjectBlend(projectId, existing.revision, previousBuffer);
|
||||
if (rollback.manifest.sha256 !== existing.sha256) {
|
||||
throw new Error("PROJECT_SAVE_ROLLBACK_FAILED: old committed hash was not restored");
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
else if (faultAt) {
|
||||
throw new Error("PROJECT_SAVE_FAULT_UNAVAILABLE: IndexedDB commits are already transactional");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ErrorReport } from "../../../protocol/error";
|
||||
import { parseSceneDelta, type SceneDelta } from "../../../protocol/scene-delta";
|
||||
import type { MeshGeometryBuffer, WebEngineLODError, WebEngineLODLevelResult, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import type { MeshGeometryBuffer, WebEngineLODError, WebEngineLODLevelResult, WebEngineOpenResourceStatus, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import { parseSceneSnapshotIR } from "../../../protocol/scene-ir";
|
||||
import { parseLODManifest, type SimplifyResult } from "../../../protocol/simplify";
|
||||
import type { LODGenerationRequest } from "../../../protocol/lod";
|
||||
@@ -95,6 +95,21 @@ let currentSnapshot: ReturnType<typeof parseSceneSnapshotIR> | null = null;
|
||||
let currentGeometryBuffers: MeshGeometryBuffer[] = [];
|
||||
let sourceBlendBuffer: ArrayBuffer | null = null;
|
||||
let sourceBlendRevision = -1;
|
||||
const cancelledOpenRequests = new Set<string>();
|
||||
const activeOpenResources = new Map<string, { liveInputBytes: number; liveNativeHandles: number; liveStagingFiles: number }>();
|
||||
|
||||
function openResourceStatus(): WebEngineOpenResourceStatus {
|
||||
return [...activeOpenResources.values()].reduce<WebEngineOpenResourceStatus>((status, item) => ({
|
||||
activeRequests: status.activeRequests + 1,
|
||||
liveInputBytes: status.liveInputBytes + item.liveInputBytes,
|
||||
liveNativeHandles: status.liveNativeHandles + item.liveNativeHandles,
|
||||
liveStagingFiles: status.liveStagingFiles + item.liveStagingFiles,
|
||||
}), { activeRequests: 0, liveInputBytes: 0, liveNativeHandles: 0, liveStagingFiles: 0 });
|
||||
}
|
||||
|
||||
function throwIfOpenCancelled(requestId: string): void {
|
||||
if (cancelledOpenRequests.has(requestId)) throw report("OPEN_CANCELLED", "Blend open cancelled before Main commit");
|
||||
}
|
||||
|
||||
function status(): WebEngineStatus {
|
||||
return {
|
||||
@@ -470,6 +485,15 @@ function nativeError(fallbackCode: ErrorReport["code"]): ErrorReport {
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
function nativeErrorFor(target: WasmModule, fallbackCode: ErrorReport["code"]): ErrorReport {
|
||||
const code = target._web_engine_last_error_code() ?? 0;
|
||||
const message = target._web_engine_last_error_message() !== 0
|
||||
? target.UTF8ToString(target._web_engine_last_error_message())
|
||||
: "WebEngine native call failed";
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : fallbackCode;
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
async function initialize(): Promise<WebEngineStatus> {
|
||||
if (module) return status();
|
||||
if (!initializationPromise) {
|
||||
@@ -921,8 +945,8 @@ async function evaluateIsolatedDecimate(
|
||||
}
|
||||
}
|
||||
|
||||
function progress(requestId: string, phase: "started" | "progress" | "completed", fraction: number, message: string): void {
|
||||
scope.postMessage({ kind: "progress", requestId, progress: { requestId, operation: "blend.open", phase, fraction, message } });
|
||||
function progress(requestId: string, phase: "started" | "progress" | "completed", fraction: number, message: string, stage?: string, cancellable?: boolean): void {
|
||||
scope.postMessage({ kind: "progress", requestId, progress: { requestId, operation: "blend.open", phase, fraction, message, stage, cancellable } });
|
||||
}
|
||||
|
||||
scope.onmessage = async (event) => {
|
||||
@@ -936,27 +960,74 @@ scope.onmessage = async (event) => {
|
||||
result = baseResult();
|
||||
break;
|
||||
case "openBlend": {
|
||||
progress(request.requestId, "started", 0, "Reading .blend");
|
||||
await initialize();
|
||||
const sourceCopy = request.command.buffer.slice(0);
|
||||
const input = copyIntoWasm(request.command.buffer);
|
||||
const resource = { liveInputBytes: request.command.buffer.byteLength, liveNativeHandles: 0, liveStagingFiles: 0 };
|
||||
activeOpenResources.set(request.requestId, resource);
|
||||
let candidateModule: WasmModule | null = null;
|
||||
let candidateHandle = 0;
|
||||
try {
|
||||
if (!module || module._web_engine_open_blend(handle, input.pointer, input.length) !== 0) {
|
||||
throw nativeError("BLEND_READ_FAILED");
|
||||
progress(request.requestId, "started", 0, "Preparing isolated Main", "NATIVE_INITIALIZE", true);
|
||||
await initialize();
|
||||
throwIfOpenCancelled(request.requestId);
|
||||
if (!wasmFactory || !wasmBinary) throw report("WASM_INIT_FAILED", "WebEngine isolated open module is unavailable");
|
||||
candidateModule = await wasmFactory({ wasmBinary: wasmBinary.slice(0) });
|
||||
throwIfOpenCancelled(request.requestId);
|
||||
candidateHandle = candidateModule._web_engine_create();
|
||||
if (candidateHandle <= 0) throw report("WASM_INIT_FAILED", "WebEngine isolated open handle creation failed");
|
||||
resource.liveNativeHandles = 1;
|
||||
const pointer = candidateModule._malloc(request.command.buffer.byteLength);
|
||||
if (!pointer) throw report("WASM_OUT_OF_MEMORY", "WebEngine could not allocate isolated blend input");
|
||||
try {
|
||||
candidateModule.HEAPU8.set(new Uint8Array(request.command.buffer), pointer);
|
||||
if (candidateModule._web_engine_open_blend(candidateHandle, pointer, request.command.buffer.byteLength) !== 0) {
|
||||
throw nativeErrorFor(candidateModule, "BLEND_READ_FAILED");
|
||||
}
|
||||
}
|
||||
finally {
|
||||
candidateModule._free(pointer);
|
||||
resource.liveInputBytes = 0;
|
||||
}
|
||||
progress(request.requestId, "progress", 0.7, "Isolated Main opened", "NATIVE_OPENED", true);
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
throwIfOpenCancelled(request.requestId);
|
||||
|
||||
const previousModule = module;
|
||||
const previousHandle = handle;
|
||||
module = candidateModule;
|
||||
handle = candidateHandle;
|
||||
try {
|
||||
progress(request.requestId, "progress", 0.8, "Building SceneIR", "COMMITTING", false);
|
||||
const scene = await readSnapshotResult();
|
||||
throwIfOpenCancelled(request.requestId);
|
||||
const sourceCopy = request.command.buffer.slice(0);
|
||||
currentSnapshot = scene.snapshot;
|
||||
sourceBlendBuffer = sourceCopy;
|
||||
sourceBlendRevision = scene.snapshot.revision;
|
||||
result = { status: status(), ...publishFullScene(scene) };
|
||||
}
|
||||
catch (error) {
|
||||
module = previousModule;
|
||||
handle = previousHandle;
|
||||
throw error;
|
||||
}
|
||||
previousModule?._web_engine_destroy(previousHandle);
|
||||
candidateHandle = 0;
|
||||
resource.liveNativeHandles = 0;
|
||||
progress(request.requestId, "completed", 1, "SceneIR ready", "COMPLETED", false);
|
||||
}
|
||||
finally {
|
||||
module?._free(input.pointer);
|
||||
if (candidateModule && candidateHandle > 0) candidateModule._web_engine_destroy(candidateHandle);
|
||||
activeOpenResources.delete(request.requestId);
|
||||
cancelledOpenRequests.delete(request.requestId);
|
||||
}
|
||||
progress(request.requestId, "progress", 0.8, "Building SceneIR");
|
||||
const scene = await readSnapshotResult();
|
||||
currentSnapshot = scene.snapshot;
|
||||
sourceBlendBuffer = sourceCopy;
|
||||
sourceBlendRevision = scene.snapshot.revision;
|
||||
result = { status: status(), ...publishFullScene(scene) };
|
||||
progress(request.requestId, "completed", 1, "SceneIR ready");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "cancelOpen":
|
||||
if (activeOpenResources.has(request.command.targetRequestId)) cancelledOpenRequests.add(request.command.targetRequestId);
|
||||
result = { status: status(), openResources: openResourceStatus() };
|
||||
break;
|
||||
case "openResourceStatus":
|
||||
result = { status: status(), openResources: openResourceStatus() };
|
||||
break;
|
||||
case "snapshot":
|
||||
await initialize();
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user