mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
feat: add manual known-word cache refresh path
- Add CLI command flag with non-GUI dispatch flow and OSD error handling. - Add runtime integration call and IPC hook so manual refresh works from command runner without app startup. - Add public AnkiIntegration manual refresh API with force refresh semantics and guard reuse. - Preserve default n+1 behavior by fixing config validation for malformed values and adding tests.
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
||||
export interface AnkiJimakuIpcDeps {
|
||||
setAnkiConnectEnabled: (enabled: boolean) => void;
|
||||
clearAnkiHistory: () => void;
|
||||
refreshKnownWords: () => Promise<void> | void;
|
||||
respondFieldGrouping: (choice: KikuFieldGroupingChoice) => void;
|
||||
buildKikuMergePreview: (
|
||||
request: KikuMergePreviewRequest,
|
||||
@@ -55,6 +56,10 @@ export function registerAnkiJimakuIpcHandlers(
|
||||
deps.clearAnkiHistory();
|
||||
});
|
||||
|
||||
ipcMain.on("anki:refresh-known-words", async () => {
|
||||
await deps.refreshKnownWords();
|
||||
});
|
||||
|
||||
ipcMain.on(
|
||||
"kiku:field-grouping-respond",
|
||||
(_event: IpcMainEvent, choice: KikuFieldGroupingChoice) => {
|
||||
|
||||
@@ -107,6 +107,7 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
|
||||
const expected = [
|
||||
"setAnkiConnectEnabled",
|
||||
"clearAnkiHistory",
|
||||
"refreshKnownWords",
|
||||
"respondFieldGrouping",
|
||||
"buildKikuMergePreview",
|
||||
"getJimakuMediaInfo",
|
||||
@@ -124,6 +125,31 @@ test("registerAnkiJimakuIpcRuntimeService provides full handler surface", () =>
|
||||
}
|
||||
});
|
||||
|
||||
test("refreshKnownWords throws when integration is unavailable", async () => {
|
||||
const { registered } = createHarness();
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await registered.refreshKnownWords();
|
||||
},
|
||||
{ message: "AnkiConnect integration not enabled" },
|
||||
);
|
||||
});
|
||||
|
||||
test("refreshKnownWords delegates to integration", async () => {
|
||||
const { registered, state } = createHarness();
|
||||
let refreshed = 0;
|
||||
state.ankiIntegration = {
|
||||
refreshKnownWordCache: async () => {
|
||||
refreshed += 1;
|
||||
},
|
||||
};
|
||||
|
||||
await registered.refreshKnownWords();
|
||||
|
||||
assert.equal(refreshed, 1);
|
||||
});
|
||||
|
||||
test("setAnkiConnectEnabled disables active integration and broadcasts changes", () => {
|
||||
const { registered, state } = createHarness();
|
||||
let destroyed = 0;
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
KikuFieldGroupingRequestData,
|
||||
} from "../../types";
|
||||
import { sortJimakuFiles } from "../../jimaku/utils";
|
||||
import { registerAnkiJimakuIpcHandlers } from "./anki-jimaku-ipc-service";
|
||||
import type { AnkiJimakuIpcDeps } from "./anki-jimaku-ipc-service";
|
||||
|
||||
export type RegisterAnkiJimakuIpcRuntimeHandler = (
|
||||
deps: AnkiJimakuIpcDeps,
|
||||
) => void;
|
||||
|
||||
interface MpvClientLike {
|
||||
connected: boolean;
|
||||
@@ -60,7 +64,7 @@ export interface AnkiJimakuIpcRuntimeOptions {
|
||||
|
||||
export function registerAnkiJimakuIpcRuntimeService(
|
||||
options: AnkiJimakuIpcRuntimeOptions,
|
||||
registerHandlers: typeof registerAnkiJimakuIpcHandlers = registerAnkiJimakuIpcHandlers,
|
||||
registerHandlers: RegisterAnkiJimakuIpcRuntimeHandler,
|
||||
): void {
|
||||
registerHandlers({
|
||||
setAnkiConnectEnabled: (enabled) => {
|
||||
@@ -108,6 +112,13 @@ export function registerAnkiJimakuIpcRuntimeService(
|
||||
console.log("AnkiConnect subtitle timing history cleared");
|
||||
}
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
const integration = options.getAnkiIntegration();
|
||||
if (!integration) {
|
||||
throw new Error("AnkiConnect integration not enabled");
|
||||
}
|
||||
await integration.refreshKnownWordCache();
|
||||
},
|
||||
respondFieldGrouping: (choice) => {
|
||||
const resolver = options.getFieldGroupingResolver();
|
||||
if (resolver) {
|
||||
|
||||
@@ -26,6 +26,7 @@ function makeArgs(overrides: Partial<CliArgs> = {}): CliArgs {
|
||||
triggerFieldGrouping: false,
|
||||
triggerSubsync: false,
|
||||
markAudioCard: false,
|
||||
refreshKnownWords: false,
|
||||
openRuntimeOptions: false,
|
||||
texthooker: false,
|
||||
help: false,
|
||||
@@ -106,6 +107,9 @@ function createDeps(overrides: Partial<CliCommandServiceDeps> = {}) {
|
||||
updateLastCardFromClipboard: async () => {
|
||||
calls.push("updateLastCardFromClipboard");
|
||||
},
|
||||
refreshKnownWords: async () => {
|
||||
calls.push("refreshKnownWords");
|
||||
},
|
||||
cycleSecondarySubMode: () => {
|
||||
calls.push("cycleSecondarySubMode");
|
||||
},
|
||||
@@ -288,3 +292,27 @@ test("handleCliCommandService handles visibility and utility command dispatches"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("handleCliCommandService runs refresh-known-words command", () => {
|
||||
const { deps, calls } = createDeps();
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
|
||||
assert.ok(calls.includes("refreshKnownWords"));
|
||||
});
|
||||
|
||||
test("handleCliCommandService reports async refresh-known-words errors to OSD", async () => {
|
||||
const { deps, calls, osd } = createDeps({
|
||||
refreshKnownWords: async () => {
|
||||
throw new Error("refresh boom");
|
||||
},
|
||||
});
|
||||
|
||||
handleCliCommandService(makeArgs({ refreshKnownWords: true }), "initial", deps);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
assert.ok(
|
||||
calls.some((value) => value.startsWith("error:refreshKnownWords failed:")),
|
||||
);
|
||||
assert.ok(osd.some((value) => value.includes("Refresh known words failed: refresh boom")));
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface CliCommandServiceDeps {
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWords: () => Promise<void>;
|
||||
cycleSecondarySubMode: () => void;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
@@ -83,6 +84,7 @@ interface MiningCliRuntime {
|
||||
mineSentenceCard: () => Promise<void>;
|
||||
startPendingMineSentenceMultiple: (timeoutMs: number) => void;
|
||||
updateLastCardFromClipboard: () => Promise<void>;
|
||||
refreshKnownWords: () => Promise<void>;
|
||||
triggerFieldGrouping: () => Promise<void>;
|
||||
triggerSubsyncFromConfig: () => Promise<void>;
|
||||
markLastCardAsAudioCard: () => Promise<void>;
|
||||
@@ -159,6 +161,7 @@ export function createCliCommandDepsRuntimeService(
|
||||
startPendingMineSentenceMultiple:
|
||||
options.mining.startPendingMineSentenceMultiple,
|
||||
updateLastCardFromClipboard: options.mining.updateLastCardFromClipboard,
|
||||
refreshKnownWords: options.mining.refreshKnownWords,
|
||||
cycleSecondarySubMode: options.ui.cycleSecondarySubMode,
|
||||
triggerFieldGrouping: options.mining.triggerFieldGrouping,
|
||||
triggerSubsyncFromConfig: options.mining.triggerSubsyncFromConfig,
|
||||
@@ -208,6 +211,7 @@ export function handleCliCommandService(
|
||||
args.mineSentence ||
|
||||
args.mineSentenceMultiple ||
|
||||
args.updateLastCardFromClipboard ||
|
||||
args.refreshKnownWords ||
|
||||
args.toggleSecondarySub ||
|
||||
args.triggerFieldGrouping ||
|
||||
args.triggerSubsync ||
|
||||
@@ -295,6 +299,13 @@ export function handleCliCommandService(
|
||||
"updateLastCardFromClipboard",
|
||||
"Update failed",
|
||||
);
|
||||
} else if (args.refreshKnownWords) {
|
||||
runAsyncWithOsd(
|
||||
() => deps.refreshKnownWords(),
|
||||
deps,
|
||||
"refreshKnownWords",
|
||||
"Refresh known words failed",
|
||||
);
|
||||
} else if (args.toggleSecondarySub) {
|
||||
deps.cycleSecondarySubMode();
|
||||
} else if (args.triggerFieldGrouping) {
|
||||
|
||||
Reference in New Issue
Block a user