Advance M8-M11 parity workflows
This commit is contained in:
160
web/protocol/diagnostic-report.ts
Normal file
160
web/protocol/diagnostic-report.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
export const APP_DIAGNOSTIC_SCHEMA_VERSION = 1 as const;
|
||||
export const MAX_APP_DIAGNOSTIC_ENTRIES = 200;
|
||||
|
||||
export const APP_DIAGNOSTIC_MESSAGES = {
|
||||
VIEWPORT_INIT_FAILED: "Viewport: unavailable",
|
||||
ACTION_CONFLICT: "Action: another project operation is running",
|
||||
ACTION_LOCK_FAILED: "Action: retry required",
|
||||
RECENT_PROJECT_LIST_FAILED: "Storage: recent projects unavailable",
|
||||
RECENT_PROJECT_REPAIR_FAILED: "Storage: recent project repair failed",
|
||||
RECENT_PROJECT_UPDATE_FAILED: "Storage: recent project update failed",
|
||||
STORAGE_BUDGET_FAILED: "Storage: budget unavailable",
|
||||
STORAGE_CLEANUP_FAILED: "Storage: cleanup failed",
|
||||
OPERATION_LOG_FAILED: "Storage: operation log unavailable",
|
||||
COMMAND_FAILED: "Engine: command failed",
|
||||
IMAGE_IMPORT_FAILED: "Image import failed",
|
||||
GREASE_PENCIL_EDIT_FAILED: "Grease Pencil edit failed",
|
||||
CURVE_EDIT_FAILED: "Curve edit failed",
|
||||
LOD_CACHE_READ_FAILED: "Storage: LOD cache unavailable; generated geometry retained",
|
||||
LOD_GENERATION_FAILED: "Engine: LOD generation failed",
|
||||
ENGINE_WORKER_TERMINATED: "Engine: Worker stopped; project remains available",
|
||||
ENGINE_START_FAILED: "Engine: unavailable",
|
||||
MANIFEST_REJECTED: "Manifest: rejected",
|
||||
STORAGE_WORKER_TERMINATED: "Storage: Worker stopped; project remains available",
|
||||
STORAGE_START_FAILED: "Storage: unavailable",
|
||||
PBR_ASSET_INVALID: "PBR asset unavailable",
|
||||
BLEND_OPEN_FAILED: "Engine: .blend open failed",
|
||||
POST_COMMIT_MAINTENANCE_FAILED: "Storage: post-commit maintenance failed",
|
||||
PROJECT_RECOVERY_FAILED: "Recovery: project could not be restored",
|
||||
WORKER_RECOVERY_FAILED: "Recovery: Worker restart failed",
|
||||
BLEND_SAVE_FAILED: "Engine: .blend save failed",
|
||||
BLEND_DOWNLOAD_FAILED: "Engine: .blend download failed",
|
||||
GLB_PROJECT_UNAVAILABLE: "GLB: no open project",
|
||||
GLB_EXPORT_BLOCKED: "GLB: export blocked",
|
||||
GLB_EXPORT_FAILED: "GLB: export failed",
|
||||
AUTOSAVE_FAILED: "Engine: autosave failed",
|
||||
} as const;
|
||||
|
||||
export type AppDiagnosticCode = keyof typeof APP_DIAGNOSTIC_MESSAGES;
|
||||
export type AppDiagnosticArea = "ACTION" | "ENGINE" | "STORAGE" | "VIEWPORT" | "EXPORT" | "RUNTIME";
|
||||
export type AppDiagnosticContextValue = string | number | boolean | null;
|
||||
|
||||
export interface AppDiagnosticEntry {
|
||||
schemaVersion: typeof APP_DIAGNOSTIC_SCHEMA_VERSION;
|
||||
sequence: number;
|
||||
occurredAt: string;
|
||||
area: AppDiagnosticArea;
|
||||
code: AppDiagnosticCode;
|
||||
summary: string;
|
||||
detail: string;
|
||||
sourceCode?: string;
|
||||
stack?: string;
|
||||
cause?: string;
|
||||
context?: Record<string, AppDiagnosticContextValue>;
|
||||
}
|
||||
|
||||
export interface AppDiagnosticReport {
|
||||
schemaVersion: typeof APP_DIAGNOSTIC_SCHEMA_VERSION;
|
||||
product: "Web Blender Modeler V1";
|
||||
generatedAt: string;
|
||||
runtime: {
|
||||
url: string;
|
||||
userAgent: string;
|
||||
language: string;
|
||||
crossOriginIsolated: boolean;
|
||||
};
|
||||
project: {
|
||||
projectId: string;
|
||||
revision: number;
|
||||
};
|
||||
entries: AppDiagnosticEntry[];
|
||||
}
|
||||
|
||||
function objectString(value: unknown): string | undefined {
|
||||
if (typeof value !== "object" || value === null) return undefined;
|
||||
try {
|
||||
const seen = new WeakSet<object>();
|
||||
return JSON.stringify(value, (_key, candidate: unknown) => {
|
||||
if (candidate instanceof Error) {
|
||||
return { name: candidate.name, message: candidate.message, stack: candidate.stack, cause: candidate.cause };
|
||||
}
|
||||
if (typeof candidate === "object" && candidate !== null) {
|
||||
if (seen.has(candidate)) return "[Circular]";
|
||||
seen.add(candidate);
|
||||
}
|
||||
return candidate;
|
||||
});
|
||||
}
|
||||
catch {
|
||||
return "[Unserializable diagnostic detail]";
|
||||
}
|
||||
}
|
||||
|
||||
function propertyString(value: unknown, property: string): string | undefined {
|
||||
if (typeof value !== "object" || value === null || !(property in value)) return undefined;
|
||||
const candidate = (value as Record<string, unknown>)[property];
|
||||
return typeof candidate === "string" && candidate ? candidate : undefined;
|
||||
}
|
||||
|
||||
function diagnosticDetail(error: unknown): Pick<AppDiagnosticEntry, "detail" | "sourceCode" | "stack" | "cause"> {
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
detail: error.message || error.name,
|
||||
sourceCode: propertyString(error, "code"),
|
||||
stack: error.stack,
|
||||
cause: error.cause instanceof Error ? error.cause.message : typeof error.cause === "string" ? error.cause : objectString(error.cause),
|
||||
};
|
||||
}
|
||||
if (typeof error === "string") return { detail: error };
|
||||
const message = propertyString(error, "message");
|
||||
const extraDetail = propertyString(error, "detail");
|
||||
return {
|
||||
detail: [message, extraDetail].filter(Boolean).join("; ") || objectString(error) || String(error),
|
||||
sourceCode: propertyString(error, "code"),
|
||||
stack: propertyString(error, "stack"),
|
||||
cause: propertyString(error, "cause"),
|
||||
};
|
||||
}
|
||||
|
||||
export function createAppDiagnosticEntry(input: {
|
||||
sequence: number;
|
||||
occurredAt: string;
|
||||
area: AppDiagnosticArea;
|
||||
code: AppDiagnosticCode;
|
||||
error: unknown;
|
||||
context?: Record<string, AppDiagnosticContextValue>;
|
||||
}): AppDiagnosticEntry {
|
||||
if (!Number.isSafeInteger(input.sequence) || input.sequence <= 0) throw new Error("APP_DIAGNOSTIC_SEQUENCE_INVALID");
|
||||
if (!Number.isFinite(Date.parse(input.occurredAt)) || new Date(input.occurredAt).toISOString() !== input.occurredAt) throw new Error("APP_DIAGNOSTIC_TIMESTAMP_INVALID");
|
||||
const detail = diagnosticDetail(input.error);
|
||||
return {
|
||||
schemaVersion: APP_DIAGNOSTIC_SCHEMA_VERSION,
|
||||
sequence: input.sequence,
|
||||
occurredAt: input.occurredAt,
|
||||
area: input.area,
|
||||
code: input.code,
|
||||
summary: APP_DIAGNOSTIC_MESSAGES[input.code],
|
||||
detail: detail.detail,
|
||||
...(detail.sourceCode ? { sourceCode: detail.sourceCode } : {}),
|
||||
...(detail.stack ? { stack: detail.stack } : {}),
|
||||
...(detail.cause ? { cause: detail.cause } : {}),
|
||||
...(input.context ? { context: { ...input.context } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function appendAppDiagnostic(entries: readonly AppDiagnosticEntry[], entry: AppDiagnosticEntry, limit = MAX_APP_DIAGNOSTIC_ENTRIES): AppDiagnosticEntry[] {
|
||||
if (!Number.isSafeInteger(limit) || limit <= 0) throw new Error("APP_DIAGNOSTIC_LIMIT_INVALID");
|
||||
return [...entries, entry].slice(-limit);
|
||||
}
|
||||
|
||||
export function createAppDiagnosticReport(input: Omit<AppDiagnosticReport, "schemaVersion" | "product" | "entries"> & { entries: readonly AppDiagnosticEntry[] }): AppDiagnosticReport {
|
||||
if (!Number.isFinite(Date.parse(input.generatedAt)) || new Date(input.generatedAt).toISOString() !== input.generatedAt) throw new Error("APP_DIAGNOSTIC_REPORT_TIMESTAMP_INVALID");
|
||||
return {
|
||||
schemaVersion: APP_DIAGNOSTIC_SCHEMA_VERSION,
|
||||
product: "Web Blender Modeler V1",
|
||||
generatedAt: input.generatedAt,
|
||||
runtime: { ...input.runtime },
|
||||
project: { ...input.project },
|
||||
entries: [...input.entries].sort((left, right) => left.sequence - right.sequence),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user