mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-27 04:49:49 -07:00
a0fec40a13
- add request timeout and HTTP status check to verify-known-word-highlights AnkiConnect client - let yomitan-script-runtime search on its own when the resolved manifest path isn't usable - dedupe repeated tier-query setup in known-word-cache-maturity tests via setTierQueries helper
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { resolveYomitanExtensionPath as resolveBuiltYomitanExtensionPath } from '../src/core/services/yomitan-extension-paths.js';
|
|
|
|
// Yomitan bootstrap for CLI scripts that need the app's real tokenizer. Mirrors
|
|
// what scripts/get_frequency.ts does inline; new scripts should import this.
|
|
export interface YomitanRuntimeState {
|
|
yomitanExt: unknown | null;
|
|
yomitanSession: unknown | null;
|
|
parserWindow: unknown | null;
|
|
parserReadyPromise: Promise<void> | null;
|
|
parserInitPromise: Promise<boolean> | null;
|
|
available: boolean;
|
|
note?: string;
|
|
}
|
|
|
|
export function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
return new Promise<T>((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
promise
|
|
.then((value) => {
|
|
clearTimeout(timer);
|
|
resolve(value);
|
|
})
|
|
.catch((error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
export function destroyParserWindow(window: unknown): void {
|
|
if (!window || typeof window !== 'object') {
|
|
return;
|
|
}
|
|
const candidate = window as { isDestroyed?: () => boolean; destroy?: () => void };
|
|
if (typeof candidate.isDestroyed !== 'function' || typeof candidate.destroy !== 'function') {
|
|
return;
|
|
}
|
|
if (!candidate.isDestroyed()) {
|
|
candidate.destroy();
|
|
}
|
|
}
|
|
|
|
export async function loadElectronModule(): Promise<typeof import('electron') | null> {
|
|
try {
|
|
const electronImport = await import('electron');
|
|
return (electronImport.default ?? electronImport) as typeof import('electron');
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function createYomitanRuntimeState(
|
|
userDataPath: string,
|
|
extensionPath?: string,
|
|
): Promise<YomitanRuntimeState> {
|
|
const state: YomitanRuntimeState = {
|
|
yomitanExt: null,
|
|
yomitanSession: null,
|
|
parserWindow: null,
|
|
parserReadyPromise: null,
|
|
parserInitPromise: null,
|
|
available: false,
|
|
};
|
|
|
|
const electronImport = await loadElectronModule();
|
|
if (
|
|
!electronImport ||
|
|
!electronImport.app ||
|
|
typeof electronImport.app.whenReady !== 'function' ||
|
|
!electronImport.session
|
|
) {
|
|
state.note = electronImport
|
|
? 'electron runtime not available in this process'
|
|
: 'electron import failed';
|
|
return state;
|
|
}
|
|
|
|
try {
|
|
await electronImport.app.whenReady();
|
|
const loadYomitanExtension = (await import('../src/core/services/yomitan-extension-loader.js'))
|
|
.loadYomitanExtension as (options: {
|
|
userDataPath: string;
|
|
extensionPath?: string;
|
|
getYomitanParserWindow: () => unknown;
|
|
setYomitanParserWindow: (window: unknown) => void;
|
|
setYomitanParserReadyPromise: (promise: Promise<void> | null) => void;
|
|
setYomitanParserInitPromise: (promise: Promise<boolean> | null) => void;
|
|
setYomitanExtension: (extension: unknown) => void;
|
|
setYomitanSession: (session: unknown) => void;
|
|
}) => Promise<unknown>;
|
|
|
|
const extension = await loadYomitanExtension({
|
|
userDataPath,
|
|
extensionPath,
|
|
getYomitanParserWindow: () => state.parserWindow,
|
|
setYomitanParserWindow: (window) => {
|
|
state.parserWindow = window;
|
|
},
|
|
setYomitanParserReadyPromise: (promise) => {
|
|
state.parserReadyPromise = promise;
|
|
},
|
|
setYomitanParserInitPromise: (promise) => {
|
|
state.parserInitPromise = promise;
|
|
},
|
|
setYomitanExtension: (loaded) => {
|
|
state.yomitanExt = loaded;
|
|
},
|
|
setYomitanSession: (nextSession) => {
|
|
state.yomitanSession = nextSession;
|
|
},
|
|
});
|
|
|
|
if (!extension) {
|
|
state.note = 'yomitan extension is not available';
|
|
return state;
|
|
}
|
|
|
|
state.yomitanExt = extension;
|
|
state.available = true;
|
|
return state;
|
|
} catch (error) {
|
|
state.note = error instanceof Error ? error.message : 'failed to initialize yomitan extension';
|
|
return state;
|
|
}
|
|
}
|
|
|
|
export async function createYomitanRuntimeStateWithSearch(
|
|
userDataPath: string,
|
|
extensionPath?: string,
|
|
): Promise<YomitanRuntimeState> {
|
|
const resolvedExtensionPath = resolveBuiltYomitanExtensionPath({
|
|
explicitPath: extensionPath,
|
|
cwd: process.cwd(),
|
|
});
|
|
|
|
if (resolvedExtensionPath) {
|
|
try {
|
|
if (fs.existsSync(path.join(resolvedExtensionPath, 'manifest.json'))) {
|
|
const state = await createYomitanRuntimeState(userDataPath, resolvedExtensionPath);
|
|
if (!state.available && !state.note) {
|
|
state.note = `Failed to load yomitan extension at ${resolvedExtensionPath}`;
|
|
}
|
|
return state;
|
|
}
|
|
} catch {
|
|
// fall through to the unconstrained loader below
|
|
}
|
|
}
|
|
|
|
// No usable manifest at the resolved path, so let the loader search on its own.
|
|
return createYomitanRuntimeState(userDataPath);
|
|
}
|