fix(svelte) fix svelte

This commit is contained in:
Giancarmine Salucci
2026-03-24 18:41:28 +01:00
parent 5f510a2237
commit 7994254e23
6 changed files with 106 additions and 50 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -1,39 +1,41 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte';
import type { IndexingJob } from '$lib/types'; import type { IndexingJob } from '$lib/types';
let { jobId }: { jobId: string } = $props(); let { jobId }: { jobId: string } = $props();
let job = $state<IndexingJob | null>(null); let job = $state<IndexingJob | null>(null);
let interval: ReturnType<typeof setInterval> | undefined;
async function pollJob() { $effect(() => {
try { // Reset and restart polling whenever jobId changes.
const res = await fetch(`/api/v1/jobs/${jobId}`); job = null;
if (res.ok) { let stopped = false;
const data = await res.json();
job = data.job; async function poll() {
if (job?.status === 'done' || job?.status === 'failed') { if (stopped) return;
if (interval !== undefined) { try {
clearInterval(interval); const res = await fetch(`/api/v1/jobs/${jobId}`);
interval = undefined; if (res.ok) {
} const data = await res.json();
job = data.job;
} }
} catch {
// ignore transient errors
} }
} catch {
// ignore polling errors
} }
}
onMount(() => { poll();
pollJob(); const interval = setInterval(() => {
interval = setInterval(pollJob, 2000); if (job?.status === 'done' || job?.status === 'failed') {
}); clearInterval(interval);
return;
}
poll();
}, 2000);
onDestroy(() => { return () => {
if (interval !== undefined) { stopped = true;
clearInterval(interval); clearInterval(interval);
} };
}); });
const progress = $derived(job?.progress ?? 0); const progress = $derived(job?.progress ?? 0);
@@ -44,7 +46,7 @@
{#if job} {#if job}
<div class="mt-2"> <div class="mt-2">
<div class="flex justify-between text-xs text-gray-500"> <div class="flex justify-between text-xs text-gray-500">
<span>{processedFiles} / {totalFiles} files</span> <span>{processedFiles.toLocaleString()} / {totalFiles.toLocaleString()} files</span>
<span>{progress}%</span> <span>{progress}%</span>
</div> </div>
<div class="mt-1 h-1.5 w-full rounded-full bg-gray-200"> <div class="mt-1 h-1.5 w-full rounded-full bg-gray-200">

View File

@@ -119,6 +119,20 @@ export class IndexingPipeline {
const filesToProcess = [...diff.added, ...diff.modified]; const filesToProcess = [...diff.added, ...diff.modified];
let processedFiles = diff.unchanged.length; // unchanged files count as processed let processedFiles = diff.unchanged.length; // unchanged files count as processed
// Report unchanged files as already processed so the progress bar
// immediately reflects real work done (especially on incremental re-index
// where most or all files are unchanged).
if (processedFiles > 0) {
const initialProgress = calculateProgress(
processedFiles,
totalFiles,
0,
0,
this.embeddingService !== null
);
this.updateJob(job.id, { processedFiles, progress: initialProgress });
}
for (const [i, file] of filesToProcess.entries()) { for (const [i, file] of filesToProcess.entries()) {
const checksum = file.sha || sha256(file.content); const checksum = file.sha || sha256(file.content);

View File

@@ -10,6 +10,46 @@ import type Database from 'better-sqlite3';
import type { IndexingJob, NewIndexingJob } from '$lib/types'; import type { IndexingJob, NewIndexingJob } from '$lib/types';
import type { IndexingPipeline } from './indexing.pipeline.js'; import type { IndexingPipeline } from './indexing.pipeline.js';
// ---------------------------------------------------------------------------
// SQL projection + row mapper (mirrors repository.service.ts pattern)
// ---------------------------------------------------------------------------
const JOB_SELECT = `
SELECT id,
repository_id AS repositoryId,
version_id AS versionId,
status, progress,
total_files AS totalFiles,
processed_files AS processedFiles,
error,
started_at AS startedAt,
completed_at AS completedAt,
created_at AS createdAt
FROM indexing_jobs`;
interface RawJob {
id: string;
repositoryId: string;
versionId: string | null;
status: 'queued' | 'running' | 'done' | 'failed';
progress: number;
totalFiles: number;
processedFiles: number;
error: string | null;
startedAt: number | null;
completedAt: number | null;
createdAt: number;
}
function mapJob(raw: RawJob): IndexingJob {
return {
...raw,
startedAt: raw.startedAt != null ? new Date(raw.startedAt * 1000) : null,
completedAt: raw.completedAt != null ? new Date(raw.completedAt * 1000) : null,
createdAt: new Date(raw.createdAt * 1000)
};
}
export class JobQueue { export class JobQueue {
private isRunning = false; private isRunning = false;
private pipeline: IndexingPipeline | null = null; private pipeline: IndexingPipeline | null = null;
@@ -30,15 +70,19 @@ export class JobQueue {
*/ */
enqueue(repositoryId: string, versionId?: string): IndexingJob { enqueue(repositoryId: string, versionId?: string): IndexingJob {
// Return early if there's already an active job for this repo. // Return early if there's already an active job for this repo.
const active = this.db const activeRaw = this.db
.prepare<[string], IndexingJob>( .prepare<[string], RawJob>(
`SELECT * FROM indexing_jobs `${JOB_SELECT}
WHERE repository_id = ? AND status IN ('queued', 'running') WHERE repository_id = ? AND status IN ('queued', 'running')
ORDER BY created_at DESC LIMIT 1` ORDER BY created_at DESC LIMIT 1`
) )
.get(repositoryId); .get(repositoryId);
if (active) return active; if (activeRaw) {
// Ensure the queue is draining even if enqueue was called concurrently.
if (!this.isRunning) setImmediate(() => this.processNext());
return mapJob(activeRaw);
}
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const job: NewIndexingJob = { const job: NewIndexingJob = {
@@ -81,9 +125,9 @@ export class JobQueue {
setImmediate(() => this.processNext()); setImmediate(() => this.processNext());
} }
return this.db return mapJob(
.prepare<[string], IndexingJob>(`SELECT * FROM indexing_jobs WHERE id = ?`) this.db.prepare<[string], RawJob>(`${JOB_SELECT} WHERE id = ?`).get(job.id as string)!
.get(job.id)!; );
} }
/** /**
@@ -97,16 +141,17 @@ export class JobQueue {
return; return;
} }
const job = this.db const rawJob = this.db
.prepare<[], IndexingJob>( .prepare<[], RawJob>(
`SELECT * FROM indexing_jobs `${JOB_SELECT}
WHERE status = 'queued' WHERE status = 'queued'
ORDER BY created_at ASC LIMIT 1` ORDER BY created_at ASC LIMIT 1`
) )
.get(); .get();
if (!job) return; if (!rawJob) return;
const job = mapJob(rawJob);
this.isRunning = true; this.isRunning = true;
try { try {
await this.pipeline.run(job); await this.pipeline.run(job);
@@ -134,11 +179,10 @@ export class JobQueue {
* Retrieve a single job by ID. * Retrieve a single job by ID.
*/ */
getJob(id: string): IndexingJob | null { getJob(id: string): IndexingJob | null {
return ( const raw = this.db
this.db .prepare<[string], RawJob>(`${JOB_SELECT} WHERE id = ?`)
.prepare<[string], IndexingJob>(`SELECT * FROM indexing_jobs WHERE id = ?`) .get(id);
.get(id) ?? null return raw ? mapJob(raw) : null;
);
} }
/** /**
@@ -163,10 +207,10 @@ export class JobQueue {
} }
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const sql = `SELECT * FROM indexing_jobs ${where} ORDER BY created_at DESC LIMIT ?`; const sql = `${JOB_SELECT} ${where} ORDER BY created_at DESC LIMIT ?`;
params.push(limit); params.push(limit);
return this.db.prepare<unknown[], IndexingJob>(sql).all(...params); return (this.db.prepare<unknown[], RawJob>(sql).all(...params) as RawJob[]).map(mapJob);
} }
/** /**

View File

@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/state';
import { get } from 'svelte/store';
import LibraryResult from '$lib/components/search/LibraryResult.svelte'; import LibraryResult from '$lib/components/search/LibraryResult.svelte';
import SnippetCard from '$lib/components/search/SnippetCard.svelte'; import SnippetCard from '$lib/components/search/SnippetCard.svelte';
import SearchInput from '$lib/components/search/SearchInput.svelte'; import SearchInput from '$lib/components/search/SearchInput.svelte';
@@ -40,14 +38,12 @@
// Initialise from URL params on mount // Initialise from URL params on mount
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
onMount(() => { $effect(() => {
const currentPage = get(page); const libParam = page.url.searchParams.get('lib');
const libParam = currentPage.url.searchParams.get('lib'); const qParam = page.url.searchParams.get('q');
const qParam = currentPage.url.searchParams.get('q');
if (libParam) { if (libParam) {
selectedLibraryId = libParam; selectedLibraryId = libParam;
// Try to restore library name from id for display.
selectedLibraryTitle = libParam; selectedLibraryTitle = libParam;
} }
if (qParam) { if (qParam) {