fix(workspace): background vector indexing and correct diagram previews
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import apiClient from './apiClient'
|
||||
|
||||
afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals() })
|
||||
|
||||
it('aborts a stuck request with an actionable timeout', async () => {
|
||||
vi.useFakeTimers()
|
||||
let signal: AbortSignal | undefined
|
||||
vi.stubGlobal('fetch', vi.fn((_url, options) => new Promise((_resolve, reject) => {
|
||||
signal = options.signal
|
||||
signal?.addEventListener('abort', () => reject(new Error('aborted')))
|
||||
})))
|
||||
const assertion = expect(apiClient.get('/api/workspace', { timeoutMs: 15000 })).rejects.toMatchObject({ code: 'REQUEST_TIMEOUT' })
|
||||
await vi.advanceTimersByTimeAsync(15000)
|
||||
await assertion
|
||||
expect(signal?.aborted).toBe(true)
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('clears the deadline after success', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response('{"ok":true}', { headers: { 'Content-Type': 'application/json' } })))
|
||||
expect(await apiClient.get('/health', { timeoutMs: 10000 })).toEqual({ ok: true })
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
@@ -9,6 +9,7 @@ export function resolveApiUrl(path: string): string {
|
||||
}
|
||||
|
||||
interface RequestOptions extends RequestInit {
|
||||
timeoutMs?: number
|
||||
params?: Record<string, string | number | boolean | undefined>
|
||||
token?: string
|
||||
}
|
||||
@@ -26,7 +27,13 @@ export class ApiErrorClass extends Error {
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { params, token, headers, ...rest } = options
|
||||
const { params, token, headers, timeoutMs, ...rest } = options
|
||||
const controller = timeoutMs ? new AbortController() : null
|
||||
let timedOut = false
|
||||
const abort = () => controller?.abort()
|
||||
if (rest.signal?.aborted) abort()
|
||||
rest.signal?.addEventListener('abort', abort, { once: true })
|
||||
const timer = timeoutMs ? setTimeout(() => { timedOut = true; controller?.abort() }, timeoutMs) : undefined
|
||||
|
||||
let url = resolveApiUrl(path)
|
||||
|
||||
@@ -54,6 +61,7 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
...rest,
|
||||
signal: controller?.signal ?? rest.signal,
|
||||
headers: reqHeaders,
|
||||
})
|
||||
|
||||
@@ -78,8 +86,12 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
|
||||
throw new ApiErrorClass(code, message, details)
|
||||
} catch (e) {
|
||||
if (timedOut) throw new ApiErrorClass('REQUEST_TIMEOUT', '请求超时,请检查后端状态后重试。')
|
||||
if (e instanceof ApiErrorClass) throw e
|
||||
throw new ApiErrorClass('NETWORK_ERROR', (e as Error).message || 'Network error')
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer)
|
||||
rest.signal?.removeEventListener('abort', abort)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ApiIndexJob, ApiIndexStatus, IndexStatus } from '@/contracts'
|
||||
|
||||
function toIndexStatus(status: ApiIndexStatus): IndexStatus {
|
||||
return {
|
||||
vector_refresh_required: status.vector_refresh_required ?? false,
|
||||
status: status.status === 'idle' ? 'idle' : status.status === 'failed' ? 'error' : 'indexing',
|
||||
pending_jobs: status.pending_jobs,
|
||||
total_notes: status.total_notes ?? null,
|
||||
@@ -13,7 +14,7 @@ function toIndexStatus(status: ApiIndexStatus): IndexStatus {
|
||||
}
|
||||
|
||||
export async function getIndexStatus(): Promise<IndexStatus> {
|
||||
return toIndexStatus(await apiClient.get<ApiIndexStatus>('/api/index/status'))
|
||||
return toIndexStatus(await apiClient.get<ApiIndexStatus>('/api/index/status', { timeoutMs: 10000 }))
|
||||
}
|
||||
|
||||
export async function rebuildIndex(scope: 'full' | 'fts' | 'vector' = 'full'): Promise<ApiIndexJob> {
|
||||
|
||||
@@ -2,13 +2,13 @@ import apiClient from './apiClient'
|
||||
import type { SystemStatus } from '@/contracts'
|
||||
|
||||
export function healthCheck(): Promise<{ status: string }> {
|
||||
return apiClient.get('/health')
|
||||
return apiClient.get('/health', { timeoutMs: 10000 })
|
||||
}
|
||||
|
||||
export function getStatus(): Promise<SystemStatus> {
|
||||
return apiClient.get('/api/status')
|
||||
return apiClient.get('/api/status', { timeoutMs: 10000 })
|
||||
}
|
||||
|
||||
export function getPermissionPolicy(): Promise<Record<string, 'allow' | 'confirm' | 'deny'>> {
|
||||
return apiClient.get('/api/permissions/policy')
|
||||
return apiClient.get('/api/permissions/policy', { timeoutMs: 10000 })
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ async function requireNoteId(filePath: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function getWorkspaceInfo(): Promise<ApiWorkspaceInfo> {
|
||||
return apiClient.get('/api/workspace')
|
||||
return apiClient.get('/api/workspace', { timeoutMs: 15000 })
|
||||
}
|
||||
|
||||
export async function getRecentVaults(): Promise<VaultInfo[]> {
|
||||
@@ -88,7 +88,7 @@ export async function getRecentVaults(): Promise<VaultInfo[]> {
|
||||
}
|
||||
|
||||
export async function openVault(path: string): Promise<VaultInfo> {
|
||||
const snapshot = await apiClient.post<ApiWorkspaceSnapshot>('/api/workspace/open', { path })
|
||||
const snapshot = await apiClient.post<ApiWorkspaceSnapshot>('/api/workspace/open', { path }, { timeoutMs: 15000 })
|
||||
cacheEntries(snapshot.items)
|
||||
return {
|
||||
vault_id: snapshot.workspace.vault_id,
|
||||
|
||||
Reference in New Issue
Block a user