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
|
||||
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
|
||||
npm install
|
||||
npm test
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
"embeddedDocuments",
|
||||
"pluginProjection",
|
||||
"securePreloadBridge",
|
||||
"windowLifecycle"
|
||||
"windowLifecycle",
|
||||
"liveProviderHost"
|
||||
],
|
||||
"supportedPlugins": [
|
||||
{
|
||||
|
|
@ -47,9 +48,14 @@
|
|||
"src/eve-electron-shell.mjs",
|
||||
"src/window-host.mjs",
|
||||
"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/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",
|
||||
"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-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 "./eve-electron-renderer.mjs";
|
||||
export * from "./live-provider-host.mjs";
|
||||
|
||||
export interface EveElectronWindowOptions extends BrowserWindowConstructorOptions {
|
||||
browserWindow?: BrowserWindowConstructorOptions;
|
||||
|
|
|
|||
|
|
@ -2,3 +2,4 @@ export * from "./eve-electron-shell.mjs";
|
|||
export * from "./eve-electron-renderer.mjs";
|
||||
export * from "./cultmesh-provider-client.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