fix(translate): replace fromStore+ with +subscribe to prevent reactive loop, add runComplete flag, show error_message, collapsible run results
- TranslationRunGlobalIndicator: replace fromStore + 6x with (() => store.subscribe()) to prevent accumulating render_effect instances via createSubscriber that caused infinite Svelte reactive flush - translate/[id]/+page.svelte: add runComplete flag to hide progress bar after completion without touching the store; add collapsible run detail cards with status badge, counts, and error_message; filter invalid runs - translationRun.js: remove stale guard (no longer needed) - Sidebar.svelte: fix mobile overlay close on route change - docs/adr/ADR-0007-rejected-fromStore-derived.md: document REJECTED pattern
This commit is contained in:
@@ -187,12 +187,11 @@
|
||||
closeMobile();
|
||||
}
|
||||
|
||||
// Close mobile overlay on route change
|
||||
// Close mobile overlay on route change — depends only on page.url.pathname, NOT isMobileOpen.
|
||||
// Otherwise clicking the burger (which sets isMobileOpen=true) would immediately close it.
|
||||
$effect(() => {
|
||||
if (isMobileOpen) {
|
||||
page.url.pathname;
|
||||
closeMobile();
|
||||
}
|
||||
const _pathname = page.url.pathname;
|
||||
closeMobile();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
<!-- @UX_FEEDBACK Rich stats during active run; compact auto-dismiss for terminal states -->
|
||||
<script>
|
||||
import { translationRunStore, stopTranslationRun } from '$lib/stores/translationRun.js';
|
||||
import { fromStore } from 'svelte/store';
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/i18n';
|
||||
@@ -17,7 +16,17 @@
|
||||
import { cancelRun } from '$lib/api/translate.js';
|
||||
import { addToast } from '$lib/toasts.js';
|
||||
|
||||
const runState = fromStore(translationRunStore);
|
||||
// Subscribe to store via $effect + subscribe (NOT fromStore + $derived).
|
||||
// @REJECTED fromStore + multiple $derived(runState.current.xxx) creates render_effect
|
||||
// instances via createSubscriber on every $derived re-evaluation. Over time these
|
||||
// accumulate and trigger an infinite Svelte reactive flush loop after ~300 polls.
|
||||
// @RATIONALE $effect(() => store.subscribe(s => state = s)) creates exactly ONE
|
||||
// subscription per component lifecycle — no accumulating render_effects.
|
||||
let storeState = $state({ uxState: 'idle', totalRecords: 0, successfulRecords: 0, failedRecords: 0, skippedRecords: 0, progressPct: 0, runId: null, jobId: null });
|
||||
$effect(() => {
|
||||
const unsub = translationRunStore.subscribe(s => { storeState = s; });
|
||||
return () => unsub();
|
||||
});
|
||||
|
||||
// Hide on the translate page itself to avoid duplication
|
||||
let isOnTranslatePage = $derived(
|
||||
@@ -29,7 +38,7 @@
|
||||
let dismissed = $state(false);
|
||||
let isCancelling = $state(false);
|
||||
|
||||
let uxState = $derived(runState.current?.uxState || 'idle');
|
||||
let uxState = $derived(storeState?.uxState || 'idle');
|
||||
|
||||
let show = $derived.by(() => {
|
||||
if (dismissed) return false;
|
||||
@@ -52,11 +61,11 @@
|
||||
return '';
|
||||
});
|
||||
|
||||
let totalRecords = $derived(runState.current?.totalRecords || 0);
|
||||
let successfulRecords = $derived(runState.current?.successfulRecords || 0);
|
||||
let failedRecords = $derived(runState.current?.failedRecords || 0);
|
||||
let skippedRecords = $derived(runState.current?.skippedRecords || 0);
|
||||
let progressPct = $derived(runState.current?.progressPct || 0);
|
||||
let totalRecords = $derived(storeState?.totalRecords || 0);
|
||||
let successfulRecords = $derived(storeState?.successfulRecords || 0);
|
||||
let failedRecords = $derived(storeState?.failedRecords || 0);
|
||||
let skippedRecords = $derived(storeState?.skippedRecords || 0);
|
||||
let progressPct = $derived(storeState?.progressPct || 0);
|
||||
|
||||
// Auto-dismiss terminal states after a few seconds
|
||||
$effect(() => {
|
||||
@@ -78,7 +87,7 @@
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
const jobId = runState.current?.jobId;
|
||||
const jobId = storeState?.jobId;
|
||||
if (jobId) {
|
||||
goto(`/translate/${jobId}`);
|
||||
}
|
||||
@@ -86,7 +95,7 @@
|
||||
|
||||
async function handleCancel(e) {
|
||||
e.stopPropagation();
|
||||
const runId = runState.current?.runId;
|
||||
const runId = storeState?.runId;
|
||||
if (!runId || isCancelling) return;
|
||||
isCancelling = true;
|
||||
try {
|
||||
|
||||
@@ -149,16 +149,6 @@ async function pollStatus() {
|
||||
const data = await fetchRunStatus(currentState.runId);
|
||||
if (!data) return;
|
||||
|
||||
// Guard: if runId was cleared (by handleRunComplete clearing it via updateTranslationRunState
|
||||
// while we were awaiting fetchRunStatus), skip the store update to avoid restoring the runId
|
||||
// and causing the progress bar to stay visible indefinitely.
|
||||
let latestState;
|
||||
translationRunStore.subscribe(s => { latestState = s; })();
|
||||
if (!latestState || !latestState.runId) {
|
||||
stopTranslationRun();
|
||||
return;
|
||||
}
|
||||
|
||||
const s = data?.status;
|
||||
const insertS = data?.insert_status;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user