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<FileResult[]>([]);
let cursor = $state<string | undefined>(undefined);
let requesting = $state(false);
let error = $state<string | null>(null);
let loadingMore = $state(false);
let moreError = $state<string | null>(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
};
};