fix(svelte): replace $effect with onMount for side effects

$effect runs during SSR and re-runs on every reactive dependency change,
causing polling loops and URL reads to fire at the wrong time. onMount
runs once on the client after first render, which is the correct lifecycle
for polling, URL param reads, and async data loads.

- IndexingProgress: polling loop now starts on mount, not on reactive trigger
- search/+page.svelte: URL param init moved to onMount; use window.location
  directly instead of the page store to avoid reactive re-runs
- settings/+page.svelte: config load and local provider probe moved to onMount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-25 14:30:04 +01:00
parent 215cadf070
commit a63de39473
3 changed files with 224 additions and 203 deletions

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { IndexingJob } from '$lib/types';
let { jobId }: { jobId: string } = $props();
let job = $state<IndexingJob | null>(null);
$effect(() => {
// Reset and restart polling whenever jobId changes.
onMount(() => {
job = null;
let stopped = false;
@@ -23,13 +23,13 @@
}
}
poll();
void poll();
const interval = setInterval(() => {
if (job?.status === 'done' || job?.status === 'failed') {
clearInterval(interval);
return;
}
poll();
void poll();
}, 2000);
return () => {

View File

@@ -1,11 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import LibraryResult from '$lib/components/search/LibraryResult.svelte';
import SnippetCard from '$lib/components/search/SnippetCard.svelte';
import SearchInput from '$lib/components/search/SearchInput.svelte';
import { copyToClipboard } from '$lib/utils/copy-to-clipboard';
import type { LibrarySearchJsonResult, SnippetJson } from '$lib/server/api/formatters';
import { onMount } from 'svelte';
// ---------------------------------------------------------------------------
// State
@@ -38,9 +38,9 @@
// Initialise from URL params on mount
// ---------------------------------------------------------------------------
$effect(() => {
const libParam = page.url.searchParams.get('lib');
const qParam = page.url.searchParams.get('q');
onMount(() => {
const libParam = new URL(window.location.href).searchParams.get('lib');
const qParam = new URL(window.location.href).searchParams.get('q');
if (libParam) {
selectedLibraryId = libParam;
@@ -50,7 +50,7 @@
query = qParam;
}
if (libParam && qParam) {
searchDocs();
void searchDocs(false);
}
});
@@ -81,7 +81,7 @@
}
}
async function searchDocs() {
async function searchDocs(syncUrl = true) {
if (!selectedLibraryId || !query.trim()) return;
loadingSnippets = true;
snippetError = null;
@@ -89,10 +89,11 @@
totalTokens = 0;
try {
const url = new URL('/api/v1/context', window.location.origin);
url.searchParams.set('libraryId', selectedLibraryId);
url.searchParams.set('query', query);
const res = await fetch(url);
const params = new URLSearchParams({
libraryId: selectedLibraryId,
query
});
const res = await fetch(`/api/v1/context?${params.toString()}`);
if (!res.ok) {
const data = await res.json();
throw new Error(data.error ?? `Request failed (${res.status})`);
@@ -101,11 +102,12 @@
snippets = data.snippets ?? [];
totalTokens = data.totalTokens ?? 0;
// Sync URL state.
if (syncUrl) {
goto(
`/search?lib=${encodeURIComponent(selectedLibraryId)}&q=${encodeURIComponent(query)}`,
{ replaceState: true, keepFocus: true }
);
}
} catch (e) {
snippetError = (e as Error).message;
} finally {

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import StatBadge from '$lib/components/StatBadge.svelte';
// ---------------------------------------------------------------------------
@@ -43,15 +44,16 @@
let saving = $state(false);
let saveStatus = $state<'idle' | 'ok' | 'error'>('idle');
let saveError = $state<string | null>(null);
let saveStatusTimer: ReturnType<typeof setTimeout> | null = null;
let localAvailable = $state<boolean | null>(null);
let loading = $state(true);
// ---------------------------------------------------------------------------
// Load current config + probe local provider on mount (Svelte 5 $effect)
// Load current config + probe local provider on mount
// ---------------------------------------------------------------------------
$effect(() => {
onMount(() => {
let cancelled = false;
(async () => {
@@ -75,12 +77,11 @@
// Probe whether the local provider is available
try {
const res = await fetch('/api/v1/settings/embedding/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: 'local' })
});
if (!cancelled) localAvailable = res.ok;
const res = await fetch('/api/v1/settings/embedding/test');
if (!cancelled && res.ok) {
const data = await res.json();
localAvailable = data.available ?? false;
}
} catch {
if (!cancelled) localAvailable = false;
}
@@ -88,19 +89,10 @@
return () => {
cancelled = true;
if (saveStatusTimer) clearTimeout(saveStatusTimer);
};
});
// Auto-dismiss save success banner after 3 seconds
$effect(() => {
if (saveStatus === 'ok') {
const timer = setTimeout(() => {
saveStatus = 'idle';
}, 3000);
return () => clearTimeout(timer);
}
});
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
@@ -151,6 +143,11 @@
});
if (res.ok) {
saveStatus = 'ok';
if (saveStatusTimer) clearTimeout(saveStatusTimer);
saveStatusTimer = setTimeout(() => {
saveStatus = 'idle';
saveStatusTimer = null;
}, 3000);
} else {
const data = await res.json();
saveStatus = 'error';
@@ -163,6 +160,11 @@
saving = false;
}
}
function handleSubmit(event: SubmitEvent) {
event.preventDefault();
void save();
}
</script>
<svelte:head>
@@ -184,10 +186,12 @@
{#if loading}
<p class="text-sm text-gray-400">Loading current configuration…</p>
{:else}
<form class="space-y-4" onsubmit={handleSubmit}>
<!-- Provider selector -->
<div class="mb-4 flex gap-2">
{#each ['none', 'openai', 'local'] as p}
<button
type="button"
onclick={() => {
provider = p as 'none' | 'openai' | 'local';
testStatus = 'idle';
@@ -219,6 +223,7 @@
<div class="flex flex-wrap gap-2">
{#each PROVIDER_PRESETS as preset}
<button
type="button"
onclick={() => applyPreset(preset)}
class="rounded border border-gray-200 px-2.5 py-1 text-xs text-gray-600 hover:bg-gray-50"
>
@@ -227,38 +232,50 @@
{/each}
</div>
<label class="block">
<label class="block" for="embedding-base-url">
<span class="text-sm font-medium text-gray-700">Base URL</span>
<input
id="embedding-base-url"
name="baseUrl"
type="text"
autocomplete="url"
bind:value={baseUrl}
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
/>
</label>
<label class="block">
<label class="block" for="embedding-api-key">
<span class="text-sm font-medium text-gray-700">API Key</span>
<input
id="embedding-api-key"
name="apiKey"
type="password"
autocomplete="off"
bind:value={apiKey}
placeholder="sk-…"
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
/>
</label>
<label class="block">
<label class="block" for="embedding-model">
<span class="text-sm font-medium text-gray-700">Model</span>
<input
id="embedding-model"
name="model"
type="text"
autocomplete="off"
bind:value={model}
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
/>
</label>
<label class="block">
<label class="block" for="embedding-dimensions">
<span class="text-sm font-medium text-gray-700">Dimensions (optional override)</span>
<input
id="embedding-dimensions"
name="dimensions"
type="number"
inputmode="numeric"
bind:value={dimensions}
class="mt-1 w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
/>
@@ -267,6 +284,7 @@
<!-- Test connection row -->
<div class="flex items-center gap-3">
<button
type="button"
onclick={testConnection}
disabled={testStatus === 'testing'}
class="rounded-lg border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-50 disabled:opacity-50"
@@ -353,13 +371,14 @@
<!-- Save row -->
<div class="mt-4 flex items-center justify-end">
<button
onclick={save}
type="submit"
disabled={saving}
class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save Settings'}
</button>
</div>
</form>
{/if}
</div>