fix(svelte) fix svelte
This commit is contained in:
BIN
local.db-shm
BIN
local.db-shm
Binary file not shown.
BIN
local.db-wal
BIN
local.db-wal
Binary file not shown.
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user