import { SvelteSet } from "svelte/reactivity"; import type { Did } from "@atcute/lexicons"; import { createBobbinClient, type BobbinContext } from "#lib/api/client.ts"; import type { FileResult } from "#lib/api/lexicons/types/org/tangled/temp/search/searchCode.ts"; import { errorMessage } from "#lib/api/load.ts"; import { searchCode } from "#lib/api/search.ts"; import { getAuth } from "#lib/auth.svelte.ts"; const PAGE_LIMIT = 50; const resultKey = (r: FileResult) => `${r.repo.did}/${r.commit}/${r.path}`; const dedupeResults = (existing: FileResult[], incoming: FileResult[]): FileResult[] => { const seen = new SvelteSet(existing.map(resultKey)); const out = [...existing]; for (const item of incoming) { const key = resultKey(item); if (!seen.has(key)) { seen.add(key); out.push(item); } } return out; }; export interface CodeSearchOptions { /** the committed query */ query: () => string; /** repo did to scope the search to. omit to search everything */ repo?: () => Did | undefined; } export const createCodeSearch = (opts: CodeSearchOptions) => { const auth = getAuth(); let results = $state([]); let cursor = $state(undefined); let requesting = $state(false); let error = $state(null); let loadingMore = $state(false); let moreError = $state(null); let session: { ctx: BobbinContext; q: string; repo: Did | undefined; signal: AbortSignal; } | null = null; const waitingForAgent = $derived(auth.currentDid !== null && auth.agent === null); const loading = $derived(requesting || (waitingForAgent && opts.query().trim() !== "")); $effect(() => { const q = opts.query().trim(); const repo = opts.repo?.(); const agent = auth.agent; if (!q || !agent) { session = null; results = []; cursor = undefined; requesting = false; loadingMore = false; error = null; moreError = null; return; } const controller = new AbortController(); const { signal } = controller; const scope = { ctx: createBobbinClient({ serviceUrl: auth.bobbinUrl, agent }), q, repo, signal }; session = scope; results = []; cursor = undefined; requesting = true; loadingMore = false; error = null; moreError = null; searchCode(scope.ctx, { q, repo, limit: PAGE_LIMIT }, { signal }) .then((found) => { if (signal.aborted) return; results = dedupeResults([], found.results); cursor = found.cursor; }) .catch((cause: unknown) => { if (signal.aborted) return; results = []; error = errorMessage(cause, "Could not search code."); }) .finally(() => { if (signal.aborted) return; requesting = false; }); return () => controller.abort(); }); const loadMore = async () => { const scope = session; if (!scope || !cursor || loadingMore) return; loadingMore = true; moreError = null; try { const found = await searchCode( scope.ctx, { q: scope.q, repo: scope.repo, limit: PAGE_LIMIT, cursor }, { signal: scope.signal } ); if (scope.signal.aborted) return; results = dedupeResults(results, found.results); cursor = found.cursor; } catch (cause: unknown) { if (scope.signal.aborted) return; moreError = errorMessage(cause, "Could not load more results."); } finally { if (!scope.signal.aborted) loadingMore = false; } }; return { get results() { return results; }, get cursor() { return cursor; }, get loading() { return loading; }, get error() { return error; }, get loadingMore() { return loadingMore; }, get moreError() { return moreError; }, loadMore }; };