perf(translation): per-message cache subscription; bounded atom families
Every text message subscribed to the whole translation cache array, so one translation re-rendered the entire timeline. Messages now subscribe to their own keyed entry, the cache key is memoised, the auto-translate "already tried" guard is module-scoped (no re-detect on virtualised remount), and both atom families are FIFO-capped and evicted on logout. Unit-tested. Fixes #39 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import {
|
||||
translationCacheAtom,
|
||||
translationEntryAtomFamily,
|
||||
msgTranslationActiveAtomFamily,
|
||||
makeCacheKey,
|
||||
findTranslation,
|
||||
addTranslation,
|
||||
wasAutoTranslateTried,
|
||||
markAutoTranslateTried,
|
||||
} from '../state/translation';
|
||||
import { chromeTranslationEngine as engine } from '../utils/translation/chromeEngine';
|
||||
import { sameLanguage } from '../utils/translation/langUtils';
|
||||
@@ -47,12 +49,17 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
|
||||
const [targetLang] = useSetting(settingsAtom, 'translateTargetLang');
|
||||
const [autoTranslate] = useSetting(settingsAtom, 'autoTranslate');
|
||||
const [active, setActive] = useAtom(msgTranslationActiveAtomFamily(eventId));
|
||||
const [cache, setCache] = useAtom(translationCacheAtom);
|
||||
// Write-only: reading the whole cache array here (via useAtom) would
|
||||
// resubscribe this message to every other message's writes. [Gitea #39]
|
||||
const setCache = useSetAtom(translationCacheAtom);
|
||||
|
||||
// The key includes a fingerprint of `text`, so an edit (same event id, new
|
||||
// body) misses the old entry and re-translates instead of showing stale text.
|
||||
const key = makeCacheKey(eventId, targetLang, text);
|
||||
const cached = findTranslation(cache, key);
|
||||
// body) misses the old entry and re-translates instead of showing stale
|
||||
// text. Memoised so the O(len) fingerprint isn't recomputed every render.
|
||||
const key = useMemo(() => makeCacheKey(eventId, targetLang, text), [eventId, targetLang, text]);
|
||||
// Subscribes to only this message's cache slot, so translating one message
|
||||
// no longer re-renders every mounted message. [Gitea #39]
|
||||
const cached = useAtomValue(translationEntryAtomFamily(key));
|
||||
|
||||
const [status, setStatus] = useState<MsgTranslationStatus>('idle');
|
||||
const [translated, setTranslated] = useState<string | undefined>(undefined);
|
||||
@@ -74,13 +81,13 @@ export const useMessageTranslation = (eventId: string, text: string): MsgTransla
|
||||
// Auto-translate: when enabled, flip `active` on for foreign messages whose
|
||||
// model is already downloaded (no gesture/download needed). Messages needing
|
||||
// a first-time download keep the manual chip (which provides the gesture).
|
||||
// Store the text we last auto-tried so an edit (new text) re-triggers auto
|
||||
// detection instead of being suppressed by a one-shot flag.
|
||||
const autoTriedText = useRef<string | undefined>(undefined);
|
||||
// The "already tried" guard is hoisted to module scope (keyed by eventId) so
|
||||
// it survives unmount — virtualised scrolling used to re-run detection every
|
||||
// time the same message remounted with a per-hook-instance ref. [Gitea #39]
|
||||
useEffect(() => {
|
||||
if (!supported || !autoTranslate || active || cached || !eventId) return;
|
||||
if (!text.trim() || autoTriedText.current === text) return;
|
||||
autoTriedText.current = text;
|
||||
if (!text.trim() || wasAutoTranslateTried(eventId, text)) return;
|
||||
markAutoTranslateTried(eventId, text);
|
||||
let alive = true;
|
||||
(async () => {
|
||||
const from = await engine.detect(text);
|
||||
|
||||
Reference in New Issue
Block a user