fix(frontend): persist active run in sessionStorage — survive tab close/reopen
Problem: closing and reopening the translate job page (or tab close+reopen)
lost the progress bar because the in-memory store starts fresh. The backend
keeps translating, but the UI shows no progress.
Fix:
- Store: save {runId, jobId, isFullRun} to sessionStorage on startTranslationRun()
via _saveToSessionStorage()
- Store: add reconnectToRun() — re-establishes WS without resetting store state,
preserving any progress data already received
- Store: add getStoredActiveRun() — public reader for sessionStorage
- Store: clear sessionStorage via _clearSessionStorage() on terminal states
(added to _cleanup() called by WS terminal, max-reconnects, stop, reset)
- Page: add that checks getStoredActiveRun() after job loads
and calls reconnectToRun() to pick up the live progress
Edge cases handled:
- Store already connected to same run → skip (no duplicate WS)
- sessionStorage unavailable → silent catch
- SPA navigation → store already has the run, WS still connected → skip
- Page reload → sessionStorage has the run, reconnects
- Tab close + reopen → same as reload
- Run completes while away → sessionStorage cleared by _cleanup()
- Run already finished → getStoredActiveRun() returns null → skip
Build ✅ 698 tests ✅
This commit is contained in:
@@ -110,6 +110,28 @@ let _timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const MAX_RECONNECT = 3;
|
||||
const RECONNECT_DELAY_MS = 10_000;
|
||||
const RUN_TIMEOUT_MS = 600_000; // 10 minutes
|
||||
const SESSION_KEY = 'ss:translate:activeRun';
|
||||
|
||||
function _saveToSessionStorage(): void {
|
||||
try {
|
||||
sessionStorage.setItem(SESSION_KEY, JSON.stringify({
|
||||
runId: _state.runId,
|
||||
jobId: _state.jobId,
|
||||
isFullRun: _state.isFullRun,
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
} catch {
|
||||
// sessionStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function _clearSessionStorage(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(SESSION_KEY);
|
||||
} catch {
|
||||
// sessionStorage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
export function clearOnCompleteCallback(): void {
|
||||
_onCompleteCallback = null;
|
||||
@@ -132,6 +154,37 @@ export function startTranslationRun(runId: string, options: StartRunOptions = {}
|
||||
_onCompleteCallback = options.onComplete || null;
|
||||
_reconnectCount = 0;
|
||||
|
||||
_saveToSessionStorage();
|
||||
_connectWebSocket(runId);
|
||||
_startTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnect to a running translation run (e.g. after page reload).
|
||||
* Unlike startTranslationRun, this does NOT clear the store first —
|
||||
* it re-establishes the WS to receive live progress.
|
||||
*/
|
||||
export function reconnectToRun(runId: string, options: StartRunOptions = {}): void {
|
||||
if (!runId) return;
|
||||
|
||||
// If already connected to this run, skip
|
||||
if (_ws && _state.runId === runId) return;
|
||||
|
||||
stopTranslationRun();
|
||||
|
||||
_state = {
|
||||
..._state,
|
||||
runId,
|
||||
uxState: 'running',
|
||||
jobId: options.jobId || _state.jobId || null,
|
||||
isFullRun: options.isFullRun || _state.isFullRun || false,
|
||||
};
|
||||
_notify();
|
||||
|
||||
_onCompleteCallback = options.onComplete || _onCompleteCallback;
|
||||
_reconnectCount = 0;
|
||||
|
||||
_saveToSessionStorage();
|
||||
_connectWebSocket(runId);
|
||||
_startTimeout();
|
||||
}
|
||||
@@ -164,6 +217,7 @@ function _clearReconnect(): void {
|
||||
function _cleanup(): void {
|
||||
_clearTimeout();
|
||||
_clearReconnect();
|
||||
_clearSessionStorage();
|
||||
if (_ws) {
|
||||
_ws.onclose = null;
|
||||
_ws.onerror = null;
|
||||
@@ -272,11 +326,25 @@ export function stopTranslationRun(): void {
|
||||
export function resetTranslationRun(): void {
|
||||
stopTranslationRun();
|
||||
_reconnectCount = 0;
|
||||
_clearSessionStorage();
|
||||
_state = initialState;
|
||||
_notify();
|
||||
_onCompleteCallback = null;
|
||||
}
|
||||
|
||||
/** Read persisted active run info from sessionStorage (survives page reload). */
|
||||
export function getStoredActiveRun(): { runId: string; jobId?: string; isFullRun?: boolean } | null {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(SESSION_KEY);
|
||||
if (!raw) return null;
|
||||
const data = JSON.parse(raw);
|
||||
if (!data?.runId) return null;
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTranslationRunState(state: Partial<TranslationRunState>): void {
|
||||
_state = { ..._state, ...state };
|
||||
_notify();
|
||||
|
||||
@@ -89,8 +89,10 @@
|
||||
import {
|
||||
translationRunStore,
|
||||
startTranslationRun,
|
||||
reconnectToRun,
|
||||
resetTranslationRun,
|
||||
clearOnCompleteCallback,
|
||||
getStoredActiveRun,
|
||||
} from '$lib/stores/translationRun.svelte.js';
|
||||
|
||||
|
||||
@@ -293,6 +295,7 @@
|
||||
// Watch for jobId changes — handles:
|
||||
// 1. Direct navigation to existing job page (environments loaded in onMount)
|
||||
// 2. Navigation after createJob (goto → param change, component reused)
|
||||
// 3. Page reload — reconnect to any active run via sessionStorage
|
||||
// Guards: only runs when environments are ready and it's an existing job
|
||||
$effect(() => {
|
||||
if (jobId && !isNewJob && environments.length > 0) {
|
||||
@@ -300,6 +303,27 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Pick up active run after job loads (survives page reload / new tab)
|
||||
$effect(() => {
|
||||
if (!existingJob || !existingJob.id) return;
|
||||
// Only run once when job first loads — avoid re-triggering on state changes
|
||||
if (!environments.length) return;
|
||||
|
||||
const stored = getStoredActiveRun();
|
||||
if (!stored || stored.jobId !== existingJob.id) return;
|
||||
// Don't reconnect if store is already connected to this run
|
||||
if (translationRunStore.value?.runId === stored.runId) return;
|
||||
|
||||
console.debug('[translate] picking up active run from sessionStorage', { runId: stored.runId });
|
||||
isRunning = true;
|
||||
isFullRun = stored.isFullRun || false;
|
||||
runComplete = false;
|
||||
reconnectToRun(stored.runId, {
|
||||
jobId: existingJob.id,
|
||||
isFullRun: stored.isFullRun || false,
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up store callback when page unmounts — the store keeps
|
||||
// polling so the global indicator in the layout still works.
|
||||
onDestroy(() => {
|
||||
|
||||
Reference in New Issue
Block a user