Advance M7 workflows and release operations
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-15 17:43:53 -04:00
parent 17ab961485
commit 7c16b279ae
103 changed files with 8064 additions and 429 deletions

View File

@@ -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");

View File

@@ -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();
{