fix(stats): refresh vocabulary aggregates after exclusion edits

- Refetch vocabulary summary/charts once the exclusion list write is server-acknowledged, and retry failed aggregate loads with backoff before showing an inline error with a Retry control
- Coalesce concurrent getVocabularySummary calls into a single worker task
- Scan imm_words in id-keyed batches to bound memory for large vocabularies
This commit is contained in:
2026-08-16 23:44:09 -07:00
parent 96d36514b5
commit 6018f393d2
10 changed files with 298 additions and 71 deletions
+12
View File
@@ -44,6 +44,16 @@ let cachedKeys: Set<string> | null = null;
let initialized: Promise<void> | null = null;
let revision = 0;
const listeners = new Set<() => void>();
// Fires only after the stats server acknowledged an exclusion write, so
// subscribers can refetch server-computed aggregates without racing the POST.
const serverSyncListeners = new Set<() => void>();
export function subscribeExcludedWordsServerSync(fn: () => void): () => void {
serverSyncListeners.add(fn);
return () => {
serverSyncListeners.delete(fn);
};
}
function readLocalStorage(): ExcludedWord[] {
if (typeof localStorage === 'undefined') return [];
@@ -104,6 +114,7 @@ export async function setExcludedWords(words: ExcludedWord[]): Promise<void> {
applyWords(normalized);
try {
await apiClient.setExcludedWords(normalized);
for (const fn of serverSyncListeners) fn();
} catch (error) {
if (revision === writeRevision) {
revision = previousRevision;
@@ -155,6 +166,7 @@ export function resetExcludedWordsStoreForTests(): void {
initialized = null;
revision = 0;
listeners.clear();
serverSyncListeners.clear();
}
function subscribe(fn: () => void): () => void {