新增 OPFS session readiness SDK helper

This commit is contained in:
2026-06-16 06:27:31 +08:00
parent d076763ef4
commit 8e37c69bc4
9 changed files with 246 additions and 2 deletions

View File

@@ -31,6 +31,7 @@ Session snapshot helpers:
- `loadSessionSnapshot()`
- `loadMachineSessionSnapshot()`
- `validateSessionSnapshot()`
- `readMachineSessionReadiness()`
Machine file and program helpers:
@@ -74,6 +75,11 @@ The browser/UI persistence workflow is host-side only:
`restoreParameters()`, `saveParameters()`, `loadToolTable()`, and
`saveToolTable()`.
External callers can run `readMachineSessionReadiness()` before step 4 to get a
structured `ready`/`blocked` report for persisted INI, parameter, tool-table,
optional G-code, and optional session snapshot files. This helper only checks
host file availability and snapshot envelope validity.
This workflow stores host files and stages them into Emscripten FS. It does not
implement G-code, tool-table, parameter-file, planner, remap, or kinematics
semantics in JavaScript.
@@ -154,6 +160,8 @@ ENABLE_PYTHON_REMAP_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_python_rem
- Invalid OPFS paths are rejected before storage access.
- Invalid snapshot format, version, session id, payload, or machine id is
rejected by snapshot validation helpers.
- `readMachineSessionReadiness()` reports missing files or invalid snapshots as
structured blocked checks before callers load a session into WASM.
- Missing INI, parameter, or tool-table OPFS files remain host persistence
errors. JavaScript does not synthesize LinuxCNC machine state.
- Tool, parameter, remap, planner, and kinematics behavior remains owned by

View File

@@ -0,0 +1,103 @@
import { loadTextFile } from "./file-service.js";
import {
gcodeProgramPath,
machineIniPath,
normalizeOpfsPath,
parameterFilePath,
sessionSnapshotPath,
toolTablePath,
} from "./path-model.js";
import { loadMachineSessionSnapshot } from "./snapshot-store.js";
function checkResult(name, path, ok, error = null) {
return {
name,
path,
ok,
error,
};
}
async function checkTextFile(name, path, storage) {
try {
const text = await loadTextFile(path, storage);
return {
...checkResult(name, path, true),
bytes: text.length,
};
} catch (error) {
return checkResult(name, path, false, error.message);
}
}
function resolveReadinessPaths(machineId, options = {}) {
const paths = {
ini: options.iniOpfsPath ?? machineIniPath(machineId, options.iniFilename),
parameters:
options.parameterOpfsPath ?? parameterFilePath(machineId, options.parameterFilename),
toolTable:
options.toolTableOpfsPath ?? toolTablePath(machineId, options.toolTableFilename),
};
if (options.gcodeOpfsPath !== undefined) {
paths.gcode = normalizeOpfsPath(options.gcodeOpfsPath);
} else if (options.gcodeFilename !== undefined) {
paths.gcode = gcodeProgramPath(options.gcodeFilename);
}
if (options.sessionId !== undefined) {
paths.snapshot = sessionSnapshotPath(options.sessionId, options.snapshotFilename);
}
return paths;
}
function summarizeReadiness(checks, snapshotResult) {
const failed = checks.filter((check) => !check.ok);
const snapshotFailed = snapshotResult && !snapshotResult.ok ? [snapshotResult] : [];
return [...failed, ...snapshotFailed];
}
export async function readMachineSessionReadiness(machineId, options = {}) {
const paths = resolveReadinessPaths(machineId, options);
const checks = [
await checkTextFile("ini", paths.ini, options.storage),
await checkTextFile("parameters", paths.parameters, options.storage),
await checkTextFile("toolTable", paths.toolTable, options.storage),
];
if (options.gcodeRequired === true || paths.gcode !== undefined) {
checks.push(await checkTextFile("gcode", paths.gcode, options.storage));
}
let snapshot = null;
let snapshotCheck = null;
if (options.sessionId !== undefined) {
try {
snapshot = await loadMachineSessionSnapshot(options.sessionId, machineId, {
storage: options.storage,
filename: options.snapshotFilename,
});
snapshotCheck = checkResult("snapshot", paths.snapshot, true);
} catch (error) {
snapshotCheck = checkResult("snapshot", paths.snapshot, false, error.message);
}
}
const failures = summarizeReadiness(checks, snapshotCheck);
return {
machineId,
ready: failures.length === 0,
phase: failures.length === 0 ? "ready" : "blocked",
paths,
checks,
snapshotCheck,
snapshot,
missing: failures.map((failure) => failure.name),
errors: failures.map((failure) => ({
name: failure.name,
path: failure.path,
error: failure.error,
})),
};
}

View File

@@ -49,6 +49,7 @@ import {
parameterFilePath,
planIniFileContextStaging,
planSimConfigStaging,
readMachineSessionReadiness,
renderIniPanelShellWorkflowOverviewEmbeddingMountState,
restoreMachineParametersFromOpfs,
saveMachineSessionSnapshot,
@@ -171,6 +172,9 @@ private module paths:
snapshot persistence.
- `machineFilePaths()`, `saveMachineTextFiles()`, `loadMachineTextFiles()`, and
`gcodeFilenameFromProgramPath()` for machine-file and G-code file helpers.
- `readMachineSessionReadiness()` for checking persisted INI, parameter,
tool-table, optional G-code, and optional session snapshot files before
loading a session.
- `restoreMachineParametersFromOpfs()`, `loadMachineToolTableFromOpfs()`, and
`loadMachineSessionFromOpfs()` for loading OPFS machine state into the
LinuxCNC-backed interpreter SDK.

View File

@@ -51,6 +51,7 @@ export {
saveMachineToolTableToOpfs,
} from "../../opfs/linuxcnc-tool-table-bridge.js";
export { loadMachineSessionFromOpfs } from "../../opfs/linuxcnc-machine-session-bridge.js";
export { readMachineSessionReadiness } from "../../opfs/machine-session-readiness.js";
export {
createIniPanelLaunchApiManifest,

View File

@@ -17,6 +17,7 @@ for (const phrase of [
"OPFS/session persistence and release gate",
"runtime/sdk/src/index.js",
"createMachineSessionSnapshotPayload",
"readMachineSessionReadiness",
"loadMachineSessionFromOpfs",
"restoreMachineParametersFromOpfs",
"loadMachineToolTableFromOpfs",
@@ -33,6 +34,7 @@ for (const phrase of [
assert.match(readmeText, /docs\/opfs-session-persistence\.md/);
assert.match(sdkReadmeText, /OPFS\/session persistence exports/);
assert.match(sdkReadmeText, /readMachineSessionReadiness/);
assert.match(hostSmokeText, /verify_sdk_surface\.sh/);
assert.match(hostSmokeText, /verify_file_service\.sh/);
assert.match(browserSmokeText, /browser_ini_opfs_smoke=ok/);

View File

@@ -43,6 +43,9 @@ import {
import {
loadMachineSessionFromOpfs,
} from "../../../runtime/opfs/linuxcnc-machine-session-bridge.js";
import {
readMachineSessionReadiness,
} from "../../../runtime/opfs/machine-session-readiness.js";
class MockFileHandle {
constructor(name) {
@@ -420,6 +423,54 @@ assertGcodeProgramStoragePath(
);
assert.equal(await loadGcodeProgram("custom-fixture.ngc", { storage }), "G1 X1 F10\nM2\n");
const readyMachineSession = await readMachineSessionReadiness("xyzab-tdr", {
storage,
sessionId: "machine-session-1",
snapshotFilename: "machine-session-snapshot.json",
gcodeFilename: "fixture.ngc",
});
assert.equal(readyMachineSession.ready, true);
assert.equal(readyMachineSession.phase, "ready");
assert.deepEqual(readyMachineSession.missing, []);
assert.deepEqual(readyMachineSession.errors, []);
assert.deepEqual(
readyMachineSession.checks.map((check) => [check.name, check.ok, check.path]),
[
["ini", true, "linuxcnc/machines/xyzab-tdr/machine.ini"],
["parameters", true, "linuxcnc/machines/xyzab-tdr/linuxcnc.var"],
["toolTable", true, "linuxcnc/machines/xyzab-tdr/tool.tbl"],
["gcode", true, "linuxcnc/gcode/fixture.ngc"],
],
);
assert.deepEqual(readyMachineSession.snapshotCheck, {
name: "snapshot",
path: "linuxcnc/sessions/machine-session-1/machine-session-snapshot.json",
ok: true,
error: null,
});
assert.equal(readyMachineSession.snapshot.sessionId, "machine-session-1");
const missingMachineSession = await readMachineSessionReadiness("missing-machine", {
storage,
sessionId: "missing-session",
gcodeRequired: true,
gcodeFilename: "missing.ngc",
});
assert.equal(missingMachineSession.ready, false);
assert.equal(missingMachineSession.phase, "blocked");
assert.deepEqual(missingMachineSession.missing, [
"ini",
"parameters",
"toolTable",
"gcode",
"snapshot",
]);
assert.deepEqual(
missingMachineSession.errors.map((error) => error.name),
["ini", "parameters", "toolTable", "gcode", "snapshot"],
);
assert.match(missingMachineSession.errors[0].error, /missing directory|missing file/);
const bridgeFiles = new Map();
const bridgeInterp = {
writeTextFile(path, text) {
@@ -897,6 +948,13 @@ await assert.rejects(
() => loadGcodeProgram("../escape.ngc", { storage }),
/Invalid G-code filename/,
);
await assert.rejects(
() => readMachineSessionReadiness("xyzab-tdr", {
storage,
gcodeOpfsPath: "../escape.ngc",
}),
/Invalid OPFS path/,
);
await assert.rejects(
() => saveSessionSnapshot("session-1", {}, {
storage,

View File

@@ -30,6 +30,7 @@ import {
parameterFilePath,
planIniFileContextStaging,
planSimConfigStaging,
readMachineSessionReadiness,
renderIniPanelShellWorkflowOverviewEmbeddingMountState,
restoreMachineParametersFromOpfs,
saveMachineSessionSnapshot,
@@ -76,6 +77,7 @@ const requiredExports = [
["restoreMachineParametersFromOpfs", restoreMachineParametersFromOpfs],
["loadMachineToolTableFromOpfs", loadMachineToolTableFromOpfs],
["loadMachineSessionFromOpfs", loadMachineSessionFromOpfs],
["readMachineSessionReadiness", readMachineSessionReadiness],
["createIniPanelLaunchApiManifest", createIniPanelLaunchApiManifest],
["isSupportedIniPanelLaunchApiManifest", isSupportedIniPanelLaunchApiManifest],
["createIniPanelShellViewModel", createIniPanelShellViewModel],