新增 OPFS session readiness SDK helper
This commit is contained in:
@@ -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
|
||||
|
||||
103
wasm-port/runtime/opfs/machine-session-readiness.js
Normal file
103
wasm-port/runtime/opfs/machine-session-readiness.js
Normal 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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user