mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-27 18:22:41 -08:00
160 lines
4.4 KiB
TypeScript
160 lines
4.4 KiB
TypeScript
import { type ReloadConfigStrictResult } from '../../config';
|
|
import type { ResolvedConfig } from '../../types';
|
|
|
|
export interface ConfigHotReloadDiff {
|
|
hotReloadFields: string[];
|
|
restartRequiredFields: string[];
|
|
}
|
|
|
|
export interface ConfigHotReloadRuntimeDeps {
|
|
getCurrentConfig: () => ResolvedConfig;
|
|
reloadConfigStrict: () => ReloadConfigStrictResult;
|
|
watchConfigPath: (configPath: string, onChange: () => void) => { close: () => void };
|
|
setTimeout: (callback: () => void, delayMs: number) => NodeJS.Timeout;
|
|
clearTimeout: (timeout: NodeJS.Timeout) => void;
|
|
debounceMs?: number;
|
|
onHotReloadApplied: (diff: ConfigHotReloadDiff, config: ResolvedConfig) => void;
|
|
onRestartRequired: (fields: string[]) => void;
|
|
onInvalidConfig: (message: string) => void;
|
|
}
|
|
|
|
export interface ConfigHotReloadRuntime {
|
|
start: () => void;
|
|
stop: () => void;
|
|
}
|
|
|
|
function isEqual(a: unknown, b: unknown): boolean {
|
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
}
|
|
|
|
function classifyDiff(prev: ResolvedConfig, next: ResolvedConfig): ConfigHotReloadDiff {
|
|
const hotReloadFields: string[] = [];
|
|
const restartRequiredFields: string[] = [];
|
|
|
|
if (!isEqual(prev.subtitleStyle, next.subtitleStyle)) {
|
|
hotReloadFields.push('subtitleStyle');
|
|
}
|
|
if (!isEqual(prev.keybindings, next.keybindings)) {
|
|
hotReloadFields.push('keybindings');
|
|
}
|
|
if (!isEqual(prev.shortcuts, next.shortcuts)) {
|
|
hotReloadFields.push('shortcuts');
|
|
}
|
|
if (prev.secondarySub.defaultMode !== next.secondarySub.defaultMode) {
|
|
hotReloadFields.push('secondarySub.defaultMode');
|
|
}
|
|
if (!isEqual(prev.ankiConnect.ai, next.ankiConnect.ai)) {
|
|
hotReloadFields.push('ankiConnect.ai');
|
|
}
|
|
|
|
const keys = new Set([
|
|
...(Object.keys(prev) as Array<keyof ResolvedConfig>),
|
|
...(Object.keys(next) as Array<keyof ResolvedConfig>),
|
|
]);
|
|
|
|
for (const key of keys) {
|
|
if (key === 'subtitleStyle' || key === 'keybindings' || key === 'shortcuts') {
|
|
continue;
|
|
}
|
|
|
|
if (key === 'secondarySub') {
|
|
const normalizedPrev = {
|
|
...prev.secondarySub,
|
|
defaultMode: next.secondarySub.defaultMode,
|
|
};
|
|
if (!isEqual(normalizedPrev, next.secondarySub)) {
|
|
restartRequiredFields.push('secondarySub');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (key === 'ankiConnect') {
|
|
const normalizedPrev = {
|
|
...prev.ankiConnect,
|
|
ai: next.ankiConnect.ai,
|
|
};
|
|
if (!isEqual(normalizedPrev, next.ankiConnect)) {
|
|
restartRequiredFields.push('ankiConnect');
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!isEqual(prev[key], next[key])) {
|
|
restartRequiredFields.push(String(key));
|
|
}
|
|
}
|
|
|
|
return { hotReloadFields, restartRequiredFields };
|
|
}
|
|
|
|
export function createConfigHotReloadRuntime(
|
|
deps: ConfigHotReloadRuntimeDeps,
|
|
): ConfigHotReloadRuntime {
|
|
let watcher: { close: () => void } | null = null;
|
|
let timer: NodeJS.Timeout | null = null;
|
|
let watchedPath: string | null = null;
|
|
const debounceMs = deps.debounceMs ?? 250;
|
|
|
|
const reloadWithDiff = () => {
|
|
const prev = deps.getCurrentConfig();
|
|
const result = deps.reloadConfigStrict();
|
|
if (!result.ok) {
|
|
deps.onInvalidConfig(`Config reload failed: ${result.error}`);
|
|
return;
|
|
}
|
|
|
|
if (watchedPath !== result.path) {
|
|
watchPath(result.path);
|
|
}
|
|
|
|
const diff = classifyDiff(prev, result.config);
|
|
if (diff.hotReloadFields.length > 0) {
|
|
deps.onHotReloadApplied(diff, result.config);
|
|
}
|
|
if (diff.restartRequiredFields.length > 0) {
|
|
deps.onRestartRequired(diff.restartRequiredFields);
|
|
}
|
|
};
|
|
|
|
const scheduleReload = () => {
|
|
if (timer) {
|
|
deps.clearTimeout(timer);
|
|
}
|
|
timer = deps.setTimeout(() => {
|
|
timer = null;
|
|
reloadWithDiff();
|
|
}, debounceMs);
|
|
};
|
|
|
|
const watchPath = (configPath: string) => {
|
|
watcher?.close();
|
|
watcher = deps.watchConfigPath(configPath, scheduleReload);
|
|
watchedPath = configPath;
|
|
};
|
|
|
|
return {
|
|
start: () => {
|
|
if (watcher) {
|
|
return;
|
|
}
|
|
const result = deps.reloadConfigStrict();
|
|
if (!result.ok) {
|
|
deps.onInvalidConfig(`Config watcher startup failed: ${result.error}`);
|
|
return;
|
|
}
|
|
watchPath(result.path);
|
|
},
|
|
stop: () => {
|
|
if (timer) {
|
|
deps.clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
watcher?.close();
|
|
watcher = null;
|
|
watchedPath = null;
|
|
},
|
|
};
|
|
}
|
|
|
|
export { classifyDiff as classifyConfigHotReloadDiff };
|