Files
cnc_wams/wasm-port/runtime/ui/ini-panel/ui-shell.js

420 lines
13 KiB
JavaScript

import {
INI_PANEL_ENTRIES,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
} from "./entry-manifest.js";
import {
createMachineSessionControlPageController,
} from "./control-page-controller.js";
import {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
import {
controlPageStatusText,
createControlPageRefreshState,
refreshMachineSessionControlPage,
} from "./control-page-refresh-workflow.js";
import {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
import {
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,
readMachineSessionReadonlyStatus,
} from "./readonly-status-api.js";
export {
controlPageStatusText,
createControlPageRefreshState,
refreshMachineSessionControlPage,
} from "./control-page-refresh-workflow.js";
export {
createControlPageSessionLoadState,
loadMachineSessionForControlPageWorkflow,
} from "./control-page-session-workflow.js";
export {
createMachineSessionReadonlyStatus,
createMachineSessionReadonlyStatusApiManifest,
readMachineSessionReadonlyStatus,
} from "./readonly-status-api.js";
export {
INI_PANEL_ENTRIES,
getIniPanelEntryByHref,
getIniPanelEntryManifest,
} from "./entry-manifest.js";
export {
createMachineSessionControlPageController,
} from "./control-page-controller.js";
export {
renderMachineSessionControlView,
} from "./control-view-renderer.js";
export function getVisibleIniPanelEntries() {
return getIniPanelEntryManifest();
}
export function createIniPanelLauncherLinkViewModel(entries = getVisibleIniPanelEntries()) {
return entries.map(({ id, href, title }) => ({
entryId: id,
href,
label: title,
}));
}
export function createIniPanelLaunchApiSchema() {
return {
apiName: "ini-panel-launch",
manifestVersion: 1,
fields: [
"apiName",
"manifestVersion",
"methods",
"visibleEntries",
"targetPages",
"controlPageApiMethods",
"persistenceCapabilities",
],
};
}
export function isSupportedIniPanelLaunchApiManifest(manifest, schema = createIniPanelLaunchApiSchema()) {
if (!manifest || typeof manifest !== "object") {
return false;
}
if (manifest.apiName !== schema.apiName || manifest.manifestVersion !== schema.manifestVersion) {
return false;
}
return schema.fields.every((fieldName) => Object.hasOwn(manifest, fieldName));
}
export function createControlPageBrowserApiSchema() {
return {
apiName: "ini-panel-control-page",
manifestVersion: 1,
fields: [
"apiName",
"manifestVersion",
"methods",
"contract",
"readonlyStatusApiManifest",
],
};
}
export function isSupportedIniPanelControlPageApiManifest(
manifest,
schema = createControlPageBrowserApiSchema(),
) {
if (!manifest || typeof manifest !== "object") {
return false;
}
if (manifest.apiName !== schema.apiName || manifest.manifestVersion !== schema.manifestVersion) {
return false;
}
return schema.fields.every((fieldName) => Object.hasOwn(manifest, fieldName));
}
export function createIniPanelLaunchApiManifest(entries = getVisibleIniPanelEntries()) {
const schema = createIniPanelLaunchApiSchema();
return {
apiName: schema.apiName,
manifestVersion: schema.manifestVersion,
methods: [
"isSupportedLaunchApiManifest",
"getLaunchApiSchema",
"getLaunchApiManifest",
"getShellIntegrationSchema",
"getShellIntegrationManifest",
"getShellIntegrationReadiness",
"getLauncherLinks",
"getControlPageApiMethods",
],
visibleEntries: entries.map(({ id, href, title }) => ({
id,
href,
title,
})),
targetPages: entries.map(({ id, href }) => ({
entryId: id,
href,
})),
controlPageApiMethods: createControlPageBrowserApiMethods(),
persistenceCapabilities: [
{
id: "machine-files",
label: "Machine text files",
scope: "opfs",
},
{
id: "session-snapshot",
label: "Machine session snapshot",
scope: "opfs",
},
{
id: "readonly-status",
label: "Read-only machine session status",
scope: "control-page",
},
],
};
}
export function createIniPanelShellControlPageContract() {
const readonlyStatusApiManifest = createMachineSessionReadonlyStatusApiManifest();
return {
methodNames: [
"createController",
"loadSessionState",
"readStatus",
"refresh",
"renderView",
],
readonlyStatusApiManifest,
readonlyStatusApiMethodNames: Object.keys(readonlyStatusApiManifest.methods),
readonlyStatusApiStatusFields: readonlyStatusApiManifest.statusFields,
relatedControlPageMethods: readonlyStatusApiManifest.relatedControlPageMethods,
};
}
export function createControlPageBrowserApiMethods() {
return [
"isSupportedControlPageApiManifest",
"getControlPageApiSchema",
"getControlPageApiManifest",
"getControlPageApiMethods",
"getControlPageContract",
"getControlView",
"getPanelApi",
"loadControlPageSessionState",
"readControlPageStatus",
"refreshControlPage",
"renderControlPage",
];
}
export function createControlPageBrowserApiManifest() {
const schema = createControlPageBrowserApiSchema();
const contract = createIniPanelShellControlPageContract();
return {
apiName: schema.apiName,
manifestVersion: schema.manifestVersion,
methods: createControlPageBrowserApiMethods(),
contract,
readonlyStatusApiManifest: contract.readonlyStatusApiManifest,
};
}
export function createIniPanelShellIntegrationSchema() {
return {
apiName: "ini-panel-shell-integration",
manifestVersion: 1,
fields: [
"apiName",
"manifestVersion",
"pages",
"launcherLinks",
"launchApiSchema",
"launchApiManifest",
"controlPageApiSchema",
"controlPageApiManifest",
"compatibility",
"persistenceCapabilities",
],
};
}
export function isSupportedIniPanelShellIntegrationManifest(
manifest,
schema = createIniPanelShellIntegrationSchema(),
) {
if (!manifest || typeof manifest !== "object") {
return false;
}
if (manifest.apiName !== schema.apiName || manifest.manifestVersion !== schema.manifestVersion) {
return false;
}
return schema.fields.every((fieldName) => Object.hasOwn(manifest, fieldName));
}
export function createIniPanelShellIntegrationManifest(entries = getVisibleIniPanelEntries()) {
const schema = createIniPanelShellIntegrationSchema();
const launchApiSchema = createIniPanelLaunchApiSchema();
const launchApiManifest = createIniPanelLaunchApiManifest(entries);
const controlPageApiSchema = createControlPageBrowserApiSchema();
const controlPageApiManifest = createControlPageBrowserApiManifest();
return {
apiName: schema.apiName,
manifestVersion: schema.manifestVersion,
pages: entries.map(({ id, href, title }) => ({
id,
href,
title,
})),
launcherLinks: createIniPanelLauncherLinkViewModel(entries),
launchApiSchema,
launchApiManifest,
controlPageApiSchema,
controlPageApiManifest,
compatibility: {
launchApiManifest: isSupportedIniPanelLaunchApiManifest(launchApiManifest, launchApiSchema),
controlPageApiManifest: isSupportedIniPanelControlPageApiManifest(
controlPageApiManifest,
controlPageApiSchema,
),
},
persistenceCapabilities: launchApiManifest.persistenceCapabilities,
};
}
export function createIniPanelShellIntegrationReadiness(
manifest = createIniPanelShellIntegrationManifest(),
) {
const integrationManifestSupported = isSupportedIniPanelShellIntegrationManifest(manifest);
const launchApiManifestSupported = integrationManifestSupported
&& isSupportedIniPanelLaunchApiManifest(manifest.launchApiManifest, manifest.launchApiSchema);
const controlPageApiManifestSupported = integrationManifestSupported
&& isSupportedIniPanelControlPageApiManifest(
manifest.controlPageApiManifest,
manifest.controlPageApiSchema,
);
const compatibility = integrationManifestSupported && manifest.compatibility
? {
launchApiManifest: Boolean(manifest.compatibility.launchApiManifest),
controlPageApiManifest: Boolean(manifest.compatibility.controlPageApiManifest),
}
: {
launchApiManifest: false,
controlPageApiManifest: false,
};
const checks = {
integrationManifestSupported,
launchApiManifestSupported,
controlPageApiManifestSupported,
launchCompatibility: compatibility.launchApiManifest,
controlPageCompatibility: compatibility.controlPageApiManifest,
};
const missing = Object.entries(checks)
.filter(([, passed]) => !passed)
.map(([name]) => name);
const ready = missing.length === 0;
return {
phase: ready ? "ready" : "blocked",
ready,
checks,
missing,
counts: {
pages: Array.isArray(manifest?.pages) ? manifest.pages.length : 0,
launcherLinks: Array.isArray(manifest?.launcherLinks) ? manifest.launcherLinks.length : 0,
launchMethods: Array.isArray(manifest?.launchApiManifest?.methods)
? manifest.launchApiManifest.methods.length
: 0,
controlPageMethods: Array.isArray(manifest?.controlPageApiManifest?.methods)
? manifest.controlPageApiManifest.methods.length
: 0,
persistenceCapabilities: Array.isArray(manifest?.persistenceCapabilities)
? manifest.persistenceCapabilities.length
: 0,
},
apiNames: {
integration: manifest?.apiName ?? null,
launch: manifest?.launchApiManifest?.apiName ?? null,
controlPage: manifest?.controlPageApiManifest?.apiName ?? null,
},
};
}
export function createIniPanelShellIntegrationWorkflowPlan({
manifest = createIniPanelShellIntegrationManifest(),
readiness = createIniPanelShellIntegrationReadiness(manifest),
controlEntryId = "control-view",
} = {}) {
const targetPage = manifest?.launchApiManifest?.targetPages?.find(
(page) => page.entryId === controlEntryId,
) ?? null;
const requiredLaunchApiMethods = [
"getShellIntegrationManifest",
"getShellIntegrationReadiness",
];
const requiredControlPageApiMethods = [
"getControlPageApiManifest",
"loadControlPageSessionState",
"readControlPageStatus",
];
const launchMethods = Array.isArray(manifest?.launchApiManifest?.methods)
? manifest.launchApiManifest.methods
: [];
const controlPageMethods = Array.isArray(manifest?.controlPageApiManifest?.methods)
? manifest.controlPageApiManifest.methods
: [];
const missingLaunchApiMethods = requiredLaunchApiMethods.filter(
(methodName) => !launchMethods.includes(methodName),
);
const missingControlPageApiMethods = requiredControlPageApiMethods.filter(
(methodName) => !controlPageMethods.includes(methodName),
);
const missing = [
...readiness.missing,
...(targetPage ? [] : ["controlTargetPage"]),
...missingLaunchApiMethods.map((methodName) => `launchApi.${methodName}`),
...missingControlPageApiMethods.map((methodName) => `controlPageApi.${methodName}`),
];
const ready = readiness.ready && missing.length === 0;
return {
phase: ready ? "ready" : "blocked",
ready,
controlEntryId,
targetPage,
readiness,
requiredLaunchApiMethods,
requiredControlPageApiMethods,
missingLaunchApiMethods,
missingControlPageApiMethods,
missing,
readonlyStatusHandoff: {
loadSessionStateMethod: "loadControlPageSessionState",
readStatusMethod: "readControlPageStatus",
expectedStatusFields: manifest?.controlPageApiManifest?.readonlyStatusApiManifest?.statusFields ?? [],
},
};
}
export function createIniPanelShellViewModel(entries = getVisibleIniPanelEntries()) {
const shellIntegrationManifest = createIniPanelShellIntegrationManifest(entries);
const shellIntegrationReadiness = createIniPanelShellIntegrationReadiness(shellIntegrationManifest);
return {
pages: entries.map(({ id, href, title }) => ({
id,
href,
title,
})),
launcherLinks: createIniPanelLauncherLinkViewModel(entries),
shellIntegrationSchema: createIniPanelShellIntegrationSchema(),
shellIntegrationManifest,
shellIntegrationReadiness,
shellIntegrationWorkflowPlan: createIniPanelShellIntegrationWorkflowPlan({
manifest: shellIntegrationManifest,
readiness: shellIntegrationReadiness,
}),
launchApiSchema: createIniPanelLaunchApiSchema(),
launchApiManifest: createIniPanelLaunchApiManifest(entries),
isSupportedShellIntegrationManifest: isSupportedIniPanelShellIntegrationManifest,
createShellIntegrationReadiness: createIniPanelShellIntegrationReadiness,
createShellIntegrationWorkflowPlan: createIniPanelShellIntegrationWorkflowPlan,
controlPage: {
browserApiSchema: createControlPageBrowserApiSchema(),
browserApiManifest: createControlPageBrowserApiManifest(),
browserApiMethods: createControlPageBrowserApiMethods(),
contract: createIniPanelShellControlPageContract(),
createController: createMachineSessionControlPageController,
isSupportedBrowserApiManifest: isSupportedIniPanelControlPageApiManifest,
loadSessionState: loadMachineSessionForControlPageWorkflow,
readonlyStatusApiManifest: createMachineSessionReadonlyStatusApiManifest(),
readStatus: readMachineSessionReadonlyStatus,
refresh: refreshMachineSessionControlPage,
renderView: renderMachineSessionControlView,
},
};
}