import type { BrowserWindow, Extension } from 'electron'; interface LoggerLike { error: (message: string, ...args: unknown[]) => void; } interface YomitanParserRuntimeDeps { getYomitanExt: () => Extension | null; getYomitanParserWindow: () => BrowserWindow | null; setYomitanParserWindow: (window: BrowserWindow | null) => void; getYomitanParserReadyPromise: () => Promise | null; setYomitanParserReadyPromise: (promise: Promise | null) => void; getYomitanParserInitPromise: () => Promise | null; setYomitanParserInitPromise: (promise: Promise | null) => void; } async function ensureYomitanParserWindow( deps: YomitanParserRuntimeDeps, logger: LoggerLike, ): Promise { const electron = await import('electron'); const yomitanExt = deps.getYomitanExt(); if (!yomitanExt) { return false; } const currentWindow = deps.getYomitanParserWindow(); if (currentWindow && !currentWindow.isDestroyed()) { return true; } const existingInitPromise = deps.getYomitanParserInitPromise(); if (existingInitPromise) { return existingInitPromise; } const initPromise = (async () => { const { BrowserWindow, session } = electron; const parserWindow = new BrowserWindow({ show: false, width: 800, height: 600, webPreferences: { contextIsolation: true, nodeIntegration: false, session: session.defaultSession, }, }); deps.setYomitanParserWindow(parserWindow); deps.setYomitanParserReadyPromise( new Promise((resolve, reject) => { parserWindow.webContents.once('did-finish-load', () => resolve()); parserWindow.webContents.once('did-fail-load', (_event, _errorCode, errorDescription) => { reject(new Error(errorDescription)); }); }), ); parserWindow.on('closed', () => { if (deps.getYomitanParserWindow() === parserWindow) { deps.setYomitanParserWindow(null); deps.setYomitanParserReadyPromise(null); } }); try { await parserWindow.loadURL(`chrome-extension://${yomitanExt.id}/search.html`); const readyPromise = deps.getYomitanParserReadyPromise(); if (readyPromise) { await readyPromise; } return true; } catch (err) { logger.error('Failed to initialize Yomitan parser window:', (err as Error).message); if (!parserWindow.isDestroyed()) { parserWindow.destroy(); } if (deps.getYomitanParserWindow() === parserWindow) { deps.setYomitanParserWindow(null); deps.setYomitanParserReadyPromise(null); } return false; } finally { deps.setYomitanParserInitPromise(null); } })(); deps.setYomitanParserInitPromise(initPromise); return initPromise; } export async function requestYomitanParseResults( text: string, deps: YomitanParserRuntimeDeps, logger: LoggerLike, ): Promise { const yomitanExt = deps.getYomitanExt(); if (!text || !yomitanExt) { return null; } const isReady = await ensureYomitanParserWindow(deps, logger); const parserWindow = deps.getYomitanParserWindow(); if (!isReady || !parserWindow || parserWindow.isDestroyed()) { return null; } const script = ` (async () => { const invoke = (action, params) => new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action, params }, (response) => { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; } if (!response || typeof response !== "object") { reject(new Error("Invalid response from Yomitan backend")); return; } if (response.error) { reject(new Error(response.error.message || "Yomitan backend error")); return; } resolve(response.result); }); }); const optionsFull = await invoke("optionsGetFull", undefined); const profileIndex = optionsFull.profileCurrent; const scanLength = optionsFull.profiles?.[profileIndex]?.options?.scanning?.length ?? 40; return await invoke("parseText", { text: ${JSON.stringify(text)}, optionsContext: { index: profileIndex }, scanLength, useInternalParser: true, useMecabParser: true }); })(); `; try { const parseResults = await parserWindow.webContents.executeJavaScript(script, true); return Array.isArray(parseResults) ? parseResults : null; } catch (err) { logger.error('Yomitan parser request failed:', (err as Error).message); return null; } }