fix(anki): avoid unnecessary known-word cache restarts

This commit is contained in:
2026-03-18 19:29:47 -07:00
parent ad1f66a842
commit 36627bf87d
2 changed files with 95 additions and 1 deletions

View File

@@ -110,3 +110,44 @@ test('AnkiIntegrationRuntime switches transports and clears known words when run
'proxy:start:127.0.0.1:8766:http://127.0.0.1:8765', 'proxy:start:127.0.0.1:8766:http://127.0.0.1:8765',
]); ]);
}); });
test('AnkiIntegrationRuntime skips known-word lifecycle restart for unrelated runtime patches', () => {
const { runtime, calls } = createRuntime({
knownWords: {
highlightEnabled: true,
},
pollingRate: 250,
});
runtime.start();
calls.length = 0;
runtime.applyRuntimeConfigPatch({
behavior: {
autoUpdateNewCards: false,
},
});
assert.deepEqual(calls, []);
});
test('AnkiIntegrationRuntime restarts known-word lifecycle when known-word settings change', () => {
const { runtime, calls } = createRuntime({
knownWords: {
highlightEnabled: true,
refreshMinutes: 90,
},
pollingRate: 250,
});
runtime.start();
calls.length = 0;
runtime.applyRuntimeConfigPatch({
knownWords: {
refreshMinutes: 120,
},
});
assert.deepEqual(calls, ['known:start']);
});

View File

@@ -145,6 +145,9 @@ export class AnkiIntegrationRuntime {
applyRuntimeConfigPatch(patch: Partial<AnkiConnectConfig>): void { applyRuntimeConfigPatch(patch: Partial<AnkiConnectConfig>): void {
const wasKnownWordCacheEnabled = this.config.knownWords?.highlightEnabled === true; const wasKnownWordCacheEnabled = this.config.knownWords?.highlightEnabled === true;
const previousKnownWordCacheConfig = wasKnownWordCacheEnabled
? this.getKnownWordCacheLifecycleConfig(this.config)
: null;
const previousTransportKey = this.getTransportConfigKey(this.config); const previousTransportKey = this.getTransportConfigKey(this.config);
const mergedConfig: AnkiConnectConfig = { const mergedConfig: AnkiConnectConfig = {
@@ -191,11 +194,19 @@ export class AnkiIntegrationRuntime {
}; };
this.config = normalizeAnkiIntegrationConfig(mergedConfig); this.config = normalizeAnkiIntegrationConfig(mergedConfig);
this.deps.onConfigChanged?.(this.config); this.deps.onConfigChanged?.(this.config);
const nextKnownWordCacheEnabled = this.config.knownWords?.highlightEnabled === true;
if (wasKnownWordCacheEnabled && this.config.knownWords?.highlightEnabled === false) { if (wasKnownWordCacheEnabled && this.config.knownWords?.highlightEnabled === false) {
this.deps.knownWordCache.stopLifecycle(); this.deps.knownWordCache.stopLifecycle();
this.deps.knownWordCache.clearKnownWordCacheState(); this.deps.knownWordCache.clearKnownWordCacheState();
} else { } else if (!wasKnownWordCacheEnabled && nextKnownWordCacheEnabled) {
this.deps.knownWordCache.startLifecycle();
} else if (
wasKnownWordCacheEnabled &&
nextKnownWordCacheEnabled &&
previousKnownWordCacheConfig !== null &&
previousKnownWordCacheConfig !== this.getKnownWordCacheLifecycleConfig(this.config)
) {
this.deps.knownWordCache.startLifecycle(); this.deps.knownWordCache.startLifecycle();
} }
@@ -206,6 +217,48 @@ export class AnkiIntegrationRuntime {
} }
} }
private getKnownWordCacheLifecycleConfig(config: AnkiConnectConfig): string {
return JSON.stringify({
refreshMinutes: this.getKnownWordRefreshIntervalMinutes(config),
scope: this.getKnownWordCacheScopeForConfig(config),
fieldsWord: trimToNonEmptyString(config.fields?.word) ?? '',
});
}
private getKnownWordRefreshIntervalMinutes(config: AnkiConnectConfig): number {
const refreshMinutes = config.knownWords?.refreshMinutes;
return typeof refreshMinutes === 'number' && Number.isFinite(refreshMinutes) && refreshMinutes > 0
? refreshMinutes
: DEFAULT_ANKI_CONNECT_CONFIG.knownWords.refreshMinutes;
}
private getKnownWordCacheScopeForConfig(config: AnkiConnectConfig): string {
const configuredDecks = config.knownWords?.decks;
if (configuredDecks && typeof configuredDecks === 'object' && !Array.isArray(configuredDecks)) {
const normalizedDecks = Object.entries(configuredDecks)
.map(([deckName, fields]) => {
const name = trimToNonEmptyString(deckName);
if (!name) return null;
const normalizedFields = Array.isArray(fields)
? [
...new Set(
fields.map(String).map(trimToNonEmptyString).filter((field): field is string => Boolean(field)),
),
].sort()
: [];
return [name, normalizedFields];
})
.filter((entry): entry is [string, string[]] => entry !== null)
.sort(([a], [b]) => a.localeCompare(b));
if (normalizedDecks.length > 0) {
return `decks:${JSON.stringify(normalizedDecks)}`;
}
}
const configuredDeck = trimToNonEmptyString(config.deck);
return configuredDeck ? `deck:${configuredDeck}` : 'is:note';
}
getOrCreateProxyServer(): AnkiIntegrationRuntimeProxyServer { getOrCreateProxyServer(): AnkiIntegrationRuntimeProxyServer {
if (!this.proxyServer) { if (!this.proxyServer) {
this.proxyServer = this.deps.proxyServerFactory(); this.proxyServer = this.deps.proxyServerFactory();