ui : persist the active backend and guard server props fetches

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 19:22:47 +02:00
parent 8ddeed94ad
commit a035f923c9
3 changed files with 59 additions and 12 deletions
@@ -16,6 +16,9 @@ export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui';
export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`;
/** Id of the backend the selector and new requests target, restored on page load. */
export const ACTIVE_BACKEND_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.activeBackend`;
/** Paused model download ids (`<repo>:<tag>`), restored on the next page load. */
export const PAUSED_MODEL_DOWNLOADS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.pausedModelDownloads`;
export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`;
+27 -6
View File
@@ -8,14 +8,34 @@
*/
import { browser } from '$app/environment';
import { LOCAL_BACKEND_ID, SETTINGS_KEYS } from '$lib/constants';
import { ACTIVE_BACKEND_LOCALSTORAGE_KEY, LOCAL_BACKEND_ID, SETTINGS_KEYS } from '$lib/constants';
import { settingsStore } from '$lib/stores/settings/index.svelte';
import type { Backend } from '$lib/types';
import { setBackendsResolver } from '$lib/utils/api-base';
import { createLocalBackend, parseBackendsSettings } from '$lib/utils/backend';
function loadActiveBackendId(): string {
if (!browser) return LOCAL_BACKEND_ID;
try {
return localStorage.getItem(ACTIVE_BACKEND_LOCALSTORAGE_KEY) ?? LOCAL_BACKEND_ID;
} catch {
return LOCAL_BACKEND_ID;
}
}
function persistActiveBackendId(backendId: string): void {
if (!browser) return;
try {
localStorage.setItem(ACTIVE_BACKEND_LOCALSTORAGE_KEY, backendId);
} catch {
/* ignore */
}
}
class BackendsStore {
activeId = $state<string>(LOCAL_BACKEND_ID);
activeId = $state<string>(loadActiveBackendId());
get active(): Backend {
const active = this.enabled.find((backend) => backend.id === this.activeId);
@@ -56,14 +76,15 @@ class BackendsStore {
this.saveExternal(this.external.filter((backend) => backend.id !== backendId));
if (this.activeId === backendId) {
this.activeId = LOCAL_BACKEND_ID;
this.setActive(LOCAL_BACKEND_ID);
}
}
setActive(backendId: string): void {
this.activeId = this.list.some((backend) => backend.id === backendId)
? backendId
: LOCAL_BACKEND_ID;
const id = this.list.some((backend) => backend.id === backendId) ? backendId : LOCAL_BACKEND_ID;
this.activeId = id;
persistActiveBackendId(id);
}
setLocalEnabled(enabled: boolean): void {
+29 -6
View File
@@ -22,6 +22,7 @@ class ServerStore {
props = $state<ApiLlamaCppServerProps | null>(null);
role = $state<ServerRole | null>(null);
status = $state<number | null>(null);
private fetchBackendId: string | undefined;
private fetchPromise: Promise<void> | null = null;
private retryTimer: ReturnType<typeof setTimeout> | null = null;
@@ -62,6 +63,7 @@ class ServerStore {
this.loading = false;
this.role = null;
this.fetchPromise = null;
this.fetchBackendId = undefined;
}
/**
@@ -70,7 +72,12 @@ class ServerStore {
* splash and the chat screen every retry tick.
*/
async fetch({ background = false }: { background?: boolean } = {}): Promise<void> {
if (this.fetchPromise) return this.fetchPromise;
// props and role describe one server. a fetch started for another backend
// must not be reused, and its response must not commit once the active
// backend has changed while it was in flight
const backendId = getBackend()?.id;
if (this.fetchPromise && this.fetchBackendId === backendId) return this.fetchPromise;
this.clearRetryTimer();
@@ -93,15 +100,20 @@ class ServerStore {
this.error = null;
}
const fetchPromise = (async () => {
const promise = (async () => {
try {
const props = await PropsService.fetch();
// the active backend changed while this request was in flight
if (getBackend()?.id !== backendId) return;
this.props = props;
this.error = null;
this.status = null;
this.detectRole(props);
} catch (error: unknown) {
if (getBackend()?.id !== backendId) return;
this.error = error instanceof Error ? error.message : String(error);
this.status = error instanceof ApiError ? error.status : null;
console.error('Error fetching server properties:', error);
@@ -113,13 +125,24 @@ class ServerStore {
if (!background) {
this.loading = false;
}
this.fetchPromise = null;
}
})();
this.fetchPromise = fetchPromise;
await fetchPromise;
this.fetchPromise = promise;
this.fetchBackendId = backendId;
// a backend switch clears the in-flight handle; only the fetch that is
// still the current one may release it
void promise
.catch(() => {})
.finally(() => {
if (this.fetchPromise === promise) {
this.fetchPromise = null;
this.fetchBackendId = undefined;
}
});
await promise;
}
private clearRetryTimer(): void {