Own live provider host lifecycle
This commit is contained in:
parent
c4d9a51f50
commit
3bce6d8911
8 changed files with 151 additions and 2 deletions
|
|
@ -19,6 +19,12 @@ Electron hosts that use the standard Eve IPC channels can point their
|
||||||
entry exposes only `window.eveProvider`; provider-specific APIs do not share the
|
entry exposes only `window.eveProvider`; provider-specific APIs do not share the
|
||||||
renderer boundary.
|
renderer boundary.
|
||||||
|
|
||||||
|
`startEveElectronProviderHost` owns the main-process runtime lifecycle: secure
|
||||||
|
window construction, generic provider client creation, Eve IPC registration,
|
||||||
|
receipt reconciliation, provider readiness, renderer loading, and cleanup.
|
||||||
|
Provider repos supply a resolved target, dependencies, renderer path, and any
|
||||||
|
provider-owned daemon development or asset hooks.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
npm install
|
npm install
|
||||||
npm test
|
npm test
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
"embeddedDocuments",
|
"embeddedDocuments",
|
||||||
"pluginProjection",
|
"pluginProjection",
|
||||||
"securePreloadBridge",
|
"securePreloadBridge",
|
||||||
"windowLifecycle"
|
"windowLifecycle",
|
||||||
|
"liveProviderHost"
|
||||||
],
|
],
|
||||||
"supportedPlugins": [
|
"supportedPlugins": [
|
||||||
{
|
{
|
||||||
|
|
@ -47,9 +48,14 @@
|
||||||
"src/eve-electron-shell.mjs",
|
"src/eve-electron-shell.mjs",
|
||||||
"src/window-host.mjs",
|
"src/window-host.mjs",
|
||||||
"src/eve-provider-preload.cjs",
|
"src/eve-provider-preload.cjs",
|
||||||
|
"src/eve-provider-preload-entry.cjs",
|
||||||
|
"src/eve-electron-renderer.mjs",
|
||||||
|
"src/live-provider-host.mjs",
|
||||||
"test/shell.test.mjs",
|
"test/shell.test.mjs",
|
||||||
"test/window-host.test.mjs",
|
"test/window-host.test.mjs",
|
||||||
"test/preload.test.cjs"
|
"test/preload.test.cjs",
|
||||||
|
"test/renderer.test.mjs",
|
||||||
|
"test/live-provider-host.test.mjs"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@
|
||||||
"types": "./src/eve-electron-renderer.d.mts",
|
"types": "./src/eve-electron-renderer.d.mts",
|
||||||
"import": "./src/eve-electron-renderer.mjs"
|
"import": "./src/eve-electron-renderer.mjs"
|
||||||
},
|
},
|
||||||
|
"./live-provider-host": {
|
||||||
|
"types": "./src/live-provider-host.d.mts",
|
||||||
|
"import": "./src/live-provider-host.mjs"
|
||||||
|
},
|
||||||
"./preload": "./src/eve-provider-preload.cjs",
|
"./preload": "./src/eve-provider-preload.cjs",
|
||||||
"./preload-entry": "./src/eve-provider-preload-entry.cjs"
|
"./preload-entry": "./src/eve-provider-preload-entry.cjs"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
1
src/index.d.ts
vendored
1
src/index.d.ts
vendored
|
|
@ -2,6 +2,7 @@ import type { BrowserWindow, BrowserWindowConstructorOptions, IpcMain, Shell } f
|
||||||
|
|
||||||
export * from "./cultmesh-provider-client.js";
|
export * from "./cultmesh-provider-client.js";
|
||||||
export * from "./eve-electron-renderer.mjs";
|
export * from "./eve-electron-renderer.mjs";
|
||||||
|
export * from "./live-provider-host.mjs";
|
||||||
|
|
||||||
export interface EveElectronWindowOptions extends BrowserWindowConstructorOptions {
|
export interface EveElectronWindowOptions extends BrowserWindowConstructorOptions {
|
||||||
browserWindow?: BrowserWindowConstructorOptions;
|
browserWindow?: BrowserWindowConstructorOptions;
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@ export * from "./eve-electron-shell.mjs";
|
||||||
export * from "./eve-electron-renderer.mjs";
|
export * from "./eve-electron-renderer.mjs";
|
||||||
export * from "./cultmesh-provider-client.mjs";
|
export * from "./cultmesh-provider-client.mjs";
|
||||||
export * from "./window-host.mjs";
|
export * from "./window-host.mjs";
|
||||||
|
export * from "./live-provider-host.mjs";
|
||||||
|
|
|
||||||
3
src/live-provider-host.d.mts
Normal file
3
src/live-provider-host.d.mts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export declare const eveProviderIpcChannels: Readonly<Record<string, string>>;
|
||||||
|
export declare function registerEveProviderIpc(ipcMain: any, client: any, options?: { receiptTimeoutMs?: number }): () => void;
|
||||||
|
export declare function startEveElectronProviderHost(options: any): Promise<{ client: any; close(): Promise<void>; window: any }>;
|
||||||
101
src/live-provider-host.mjs
Normal file
101
src/live-provider-host.mjs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { EveCultMeshProviderClient } from "./cultmesh-provider-client.mjs";
|
||||||
|
import { createEveElectronWindow, registerEveWindowControls } from "./window-host.mjs";
|
||||||
|
|
||||||
|
export const eveProviderIpcChannels = Object.freeze({
|
||||||
|
providerAdvertisement: "eve:provider-advertisement",
|
||||||
|
surface: "eve:surface",
|
||||||
|
document: "eve:embedded-document",
|
||||||
|
submitCommand: "eve:submit-command",
|
||||||
|
windowControl: "eve-electron:window-control",
|
||||||
|
});
|
||||||
|
|
||||||
|
export function registerEveProviderIpc(ipcMain, client, { receiptTimeoutMs = 5000 } = {}) {
|
||||||
|
const handlers = [
|
||||||
|
[eveProviderIpcChannels.providerAdvertisement, () => client.providerAdvertisement()],
|
||||||
|
[eveProviderIpcChannels.surface, (_event, request) => client.surface(request?.surfaceId)],
|
||||||
|
[eveProviderIpcChannels.document, (_event, request) => client.resolveDocument(request)],
|
||||||
|
[eveProviderIpcChannels.submitCommand, async (_event, request) => {
|
||||||
|
const submission = await client.submitCommand(request);
|
||||||
|
const receipt = await waitForReceipt(client, submission, receiptTimeoutMs);
|
||||||
|
return { ...submission, ...receipt, accepted: receiptAccepted(receipt) };
|
||||||
|
}],
|
||||||
|
];
|
||||||
|
for (const [channel, handler] of handlers) ipcMain.handle(channel, handler);
|
||||||
|
return () => {
|
||||||
|
for (const [channel] of handlers) ipcMain.removeHandler(channel);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startEveElectronProviderHost(options) {
|
||||||
|
const { app, BrowserWindow, ipcMain, shell } = options.electron;
|
||||||
|
await app.whenReady();
|
||||||
|
const client = (options.createProviderClient || ((target, dependencies, clientOptions) =>
|
||||||
|
new EveCultMeshProviderClient(target, dependencies, clientOptions)))(
|
||||||
|
options.providerTarget,
|
||||||
|
options.dependencies,
|
||||||
|
{ runtimeId: options.runtimeId || "eve-electron" },
|
||||||
|
);
|
||||||
|
const window = createEveElectronWindow({
|
||||||
|
...(options.window || {}),
|
||||||
|
preload: options.preload || fileURLToPath(new URL("./eve-provider-preload-entry.cjs", import.meta.url)),
|
||||||
|
}, { BrowserWindow, shell });
|
||||||
|
const removeProviderIpc = registerEveProviderIpc(ipcMain, client, options.receipts);
|
||||||
|
const removeWindowIpc = registerEveWindowControls(ipcMain, BrowserWindow, eveProviderIpcChannels.windowControl);
|
||||||
|
let closed = false;
|
||||||
|
const close = async () => {
|
||||||
|
if (closed) return;
|
||||||
|
closed = true;
|
||||||
|
removeProviderIpc();
|
||||||
|
removeWindowIpc();
|
||||||
|
await client.close();
|
||||||
|
if (!window.isDestroyed?.()) window.destroy();
|
||||||
|
};
|
||||||
|
app.on("before-quit", () => void close());
|
||||||
|
app.on("window-all-closed", () => app.quit());
|
||||||
|
try {
|
||||||
|
await waitForProvider(client, options.providerTimeoutMs ?? 30000);
|
||||||
|
await window.loadFile(options.renderer, { query: { surface: options.surfaceId || "" } });
|
||||||
|
return { client, close, window };
|
||||||
|
} catch (error) {
|
||||||
|
await close();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForProvider(client, timeoutMs) {
|
||||||
|
const started = Date.now();
|
||||||
|
let lastError;
|
||||||
|
while (Date.now() - started < timeoutMs) {
|
||||||
|
try {
|
||||||
|
return await client.providerAdvertisement();
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await delay(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError || new Error("Timed out waiting for Eve provider advertisement.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForReceipt(client, submission, timeoutMs) {
|
||||||
|
const started = Date.now();
|
||||||
|
let lastError;
|
||||||
|
while (Date.now() - started < timeoutMs) {
|
||||||
|
try {
|
||||||
|
return await client.receipt(submission);
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
await delay(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError || new Error(`Timed out waiting for Eve receipt ${submission.commandId || ""}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiptAccepted(receipt) {
|
||||||
|
const state = String(receipt?.state || "").toLowerCase();
|
||||||
|
return state === "accepted" || state === "reconciled";
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(milliseconds) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||||
|
}
|
||||||
27
test/live-provider-host.test.mjs
Normal file
27
test/live-provider-host.test.mjs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { eveProviderIpcChannels, registerEveProviderIpc } from "../src/live-provider-host.mjs";
|
||||||
|
|
||||||
|
test("registers one generic Eve IPC authority and removes it cleanly", async () => {
|
||||||
|
const handlers = new Map();
|
||||||
|
const ipcMain = { handle: (channel, handler) => handlers.set(channel, handler), removeHandler: channel => handlers.delete(channel) };
|
||||||
|
const client = {
|
||||||
|
providerAdvertisement: async () => ({ providerId: "fixture" }),
|
||||||
|
surface: async surfaceId => ({ surfaceId }),
|
||||||
|
resolveDocument: async request => request,
|
||||||
|
submitCommand: async () => ({ commandId: "command-1", receiptSchema: "receipt.v1", receiptRecordRef: "receipts:command-1" }),
|
||||||
|
receipt: async () => ({ commandId: "command-1", state: "reconciled" }),
|
||||||
|
};
|
||||||
|
const remove = registerEveProviderIpc(ipcMain, client);
|
||||||
|
assert.deepEqual([...handlers.keys()], [
|
||||||
|
eveProviderIpcChannels.providerAdvertisement,
|
||||||
|
eveProviderIpcChannels.surface,
|
||||||
|
eveProviderIpcChannels.document,
|
||||||
|
eveProviderIpcChannels.submitCommand,
|
||||||
|
]);
|
||||||
|
assert.deepEqual(await handlers.get(eveProviderIpcChannels.submitCommand)(null, { command: "move" }), {
|
||||||
|
commandId: "command-1", receiptSchema: "receipt.v1", receiptRecordRef: "receipts:command-1", state: "reconciled", accepted: true,
|
||||||
|
});
|
||||||
|
remove();
|
||||||
|
assert.equal(handlers.size, 0);
|
||||||
|
});
|
||||||
Reference in a new issue