Files
FreeCADWasmDev/web/src/spreadsheet-python-family-facade.ts
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

85 lines
7.6 KiB
TypeScript

import { FacadeHandlerError, FacadeRequest, FacadeResponse, FacadeSerialQueue, createRequest, isFacadeRequest } from "./facade-envelope.js";
export const SPREADSHEET_PYTHON_FAMILY_SCHEMA = "freecad-web-spreadsheet/python-family-batch/v1" as const;
export const SPREADSHEET_PYTHON_FAMILY_COMMAND = "spreadsheet.pythonFamilyBatch" as const;
/** Native-backed Spreadsheet desktop commands exposed through the generic family dispatcher. */
export const SPREADSHEET_DESKTOP_COMMANDS = [
"Spreadsheet_AlignTop",
"Spreadsheet_AlignVCenter",
"Spreadsheet_AlignBottom",
"Spreadsheet_AlignCenter",
"Spreadsheet_AlignLeft",
"Spreadsheet_AlignRight",
"Spreadsheet_CreateSheet",
"Spreadsheet_Export",
"Spreadsheet_Import",
"Spreadsheet_MergeCells",
"Spreadsheet_SetAlias",
"Spreadsheet_SplitCell",
"Spreadsheet_StyleBold",
"Spreadsheet_StyleItalic",
"Spreadsheet_StyleUnderline",
] as const;
export type SpreadsheetDesktopCommand = typeof SPREADSHEET_DESKTOP_COMMANDS[number];
export type SpreadsheetPythonFamilyTier = "tier-1-readonly" | "tier-2-pure" | "object-contract";
export type SpreadsheetPythonFamilyStatus = "exposed" | "restricted" | "nonportable" | "not_implemented";
export interface SpreadsheetPythonFamilyRecord { apiId: string; name: string; family: string; sourcePath?: string; disposition?: string; args?: Record<string, unknown>; }
export interface SpreadsheetPythonFamilyPayload { schema: typeof SPREADSHEET_PYTHON_FAMILY_SCHEMA; module: "Spreadsheet"; tier: SpreadsheetPythonFamilyTier; records: SpreadsheetPythonFamilyRecord[]; }
export interface SpreadsheetPythonFamilyNativeRecord extends SpreadsheetPythonFamilyRecord { status: SpreadsheetPythonFamilyStatus; code: string; message: string; native: true; resourcesReleased: true; sourceEntry: string; sourceRevision: string; parameterSchema?: string; returnSchema?: string; replay?: string; }
export interface SpreadsheetPythonFamilyResult { schema: typeof SPREADSHEET_PYTHON_FAMILY_SCHEMA; module: "Spreadsheet"; tier: SpreadsheetPythonFamilyTier; source: "freecad-source"; records: SpreadsheetPythonFamilyNativeRecord[]; count: number; resourcesReleased: true; }
export type SpreadsheetPythonFamilyRequest = FacadeRequest<SpreadsheetPythonFamilyPayload>;
export type SpreadsheetPythonFamilyResponse = FacadeResponse<SpreadsheetPythonFamilyResult>;
export interface SpreadsheetPythonFamilyKernel { ccall(name: string, returnType: "number" | "string", argTypes?: string[], args?: unknown[]): number | string; }
export function createSpreadsheetPythonFamilyRequest(requestId: string, records: SpreadsheetPythonFamilyRecord[], tier: SpreadsheetPythonFamilyTier = "tier-1-readonly"): SpreadsheetPythonFamilyRequest {
return createRequest(requestId, SPREADSHEET_PYTHON_FAMILY_COMMAND, { schema: SPREADSHEET_PYTHON_FAMILY_SCHEMA, module: "Spreadsheet", tier, records }, "spreadsheet-python-family");
}
/** Build a React-visible command record without introducing a transport alias or fixture payload. */
export function createSpreadsheetDesktopCommandRecord(command: SpreadsheetDesktopCommand, args: Record<string, unknown> = {}): SpreadsheetPythonFamilyRecord {
return { apiId: `Spreadsheet:desktop:${command}`, name: command, family: "desktop-command", sourcePath: "freecad-source/src/Mod/Spreadsheet/Gui/Command.cpp", args };
}
export function isSpreadsheetPythonFamilyRequest(value: unknown): value is SpreadsheetPythonFamilyRequest {
if (!isFacadeRequest(value)) return false;
const request = value as SpreadsheetPythonFamilyRequest;
const payload = request.payload;
return request.command === SPREADSHEET_PYTHON_FAMILY_COMMAND
&& payload?.schema === SPREADSHEET_PYTHON_FAMILY_SCHEMA
&& payload?.module === "Spreadsheet"
&& (payload?.tier === "tier-1-readonly" || payload?.tier === "tier-2-pure" || payload?.tier === "object-contract")
&& Array.isArray(payload.records)
&& payload.records.length > 0
&& payload.records.every((record) => record && typeof record.apiId === "string" && typeof record.name === "string" && typeof record.family === "string" && typeof record.sourcePath === "string");
}
export async function handleSpreadsheetPythonFamily(payload: SpreadsheetPythonFamilyPayload, signal: AbortSignal, source: SpreadsheetPythonFamilyKernel | Promise<SpreadsheetPythonFamilyKernel>): Promise<SpreadsheetPythonFamilyResult> {
if (signal.aborted) throw new FacadeHandlerError("cancelled", "request was cancelled");
if (!isSpreadsheetPythonFamilyRequest(createSpreadsheetPythonFamilyRequest("validation", payload.records, payload.tier))) throw new FacadeHandlerError("invalid_fixture", "Spreadsheet Python family payload is invalid");
const kernel = await source;
if (signal.aborted) throw new FacadeHandlerError("cancelled", "request was cancelled");
let raw: string;
try { raw = String(kernel.ccall("fc_web_dispatch_json", "string", ["string"], [JSON.stringify({ cmd: SPREADSHEET_PYTHON_FAMILY_COMMAND, args: { tier: payload.tier, records: payload.records } })])); }
catch (error) { throw new FacadeHandlerError("native_dispatch_failed", error instanceof Error ? error.message : "Spreadsheet native dispatch failed"); }
let reply: unknown;
try { reply = JSON.parse(raw); } catch { throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned invalid JSON"); }
if (!reply || typeof reply !== "object") throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned a non-object result");
const result = reply as Record<string, unknown>;
if (result.ok !== true) throw new FacadeHandlerError(String(result.code || "native_error"), String(result.error || "Spreadsheet Python family dispatch failed"));
if (result.schema !== SPREADSHEET_PYTHON_FAMILY_SCHEMA || result.module !== "Spreadsheet" || result.tier !== payload.tier || result.source !== "freecad-source" || result.count !== payload.records.length || result.resourcesReleased !== true || !Array.isArray(result.records) || result.records.length !== payload.records.length) throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned an invalid batch schema");
const terminal = new Set<SpreadsheetPythonFamilyStatus>(["exposed", "restricted", "nonportable", "not_implemented"]);
for (let index = 0; index < payload.records.length; index += 1) {
const requested = payload.records[index];
const native = result.records[index];
if (!native || typeof native !== "object") throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned a non-object record", { index });
const record = native as Record<string, unknown>;
if (record.apiId !== requested.apiId || record.name !== requested.name || record.family !== requested.family || record.sourceEntry !== requested.sourcePath) throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned a record identity mismatch", { index, expectedApiId: requested.apiId, actualApiId: record.apiId });
if (!terminal.has(String(record.status) as SpreadsheetPythonFamilyStatus) || record.native !== true || record.resourcesReleased !== true || typeof record.sourceRevision !== "string") throw new FacadeHandlerError("native_protocol_error", "Spreadsheet native dispatcher returned an invalid record status", { index, apiId: requested.apiId });
}
return result as unknown as SpreadsheetPythonFamilyResult;
}
export function dispatchSpreadsheetPythonFamily(queue: FacadeSerialQueue, request: SpreadsheetPythonFamilyRequest, source: SpreadsheetPythonFamilyKernel | Promise<SpreadsheetPythonFamilyKernel>): Promise<SpreadsheetPythonFamilyResponse> {
return queue.dispatch(request, (payload, signal) => handleSpreadsheetPythonFamily(payload, signal, source));
}