import type { DiffOptions } from "@bindings/cob/DiffOptions"; import type { Diff } from "@bindings/diff/Diff"; import type { Stats } from "@bindings/diff/Stats"; import type { Commit } from "@bindings/repo/Commit"; import type { Job } from "@bindings/repo/Job"; import type { RepoSummary } from "@bindings/repo/RepoSummary"; import * as tauri from "@tauri-apps/api/core"; import { cached } from "@app/lib/cached"; export async function invoke( cmd: string, args?: tauri.InvokeArgs, options?: tauri.InvokeOptions, ): Promise { if (import.meta.env.DEV) { const start = performance.now(); try { return await withTestBackend(tauri.invoke, cmd, args, options); } finally { const ms = Math.round(performance.now() - start); console.debug(`[invoke] ${cmd} ${ms}ms`); } } return withTestBackend(tauri.invoke, cmd, args, options); } /** * Invoking a Tauri command returned an error with the given message */ class InvokeError extends Error { name = "InvokeError"; code: string; constructor(message: string, code = "Unknown Error") { super(message); this.code = code; Error.captureStackTrace?.(this, InvokeError); } } async function withTestBackend( fn: ( cmd: string, args?: tauri.InvokeArgs, options?: tauri.InvokeOptions, ) => Promise, cmd: string, args?: tauri.InvokeArgs, options?: tauri.InvokeOptions, ) { if (window.__TAURI_INTERNALS__) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return fn(cmd, args, options).catch((error: any) => { if (typeof error === "object" && error !== null && "message" in error) { throw new InvokeError(String(error.message), String(error.code)); } else { throw new InvokeError(`Invalid error object: ${error}`); } }); } else { return fetch( `http://127.0.0.1:${import.meta.env.VITE_TEST_HTTP_API_PORT ?? 8081}/${cmd}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(args), }, ).then(async response => { if (response.status === 404) { console.log("Got a 404 response:", response); return null; } const json = await response.json(); if (!response.ok) { if (typeof json === "object" && json !== null && "message" in json) { throw new InvokeError(String(json.message), String(json.code)); } else { throw new InvokeError(`Invalid error object: ${json}`); } } return json; }); } } async function getDiff(rid: string, options: DiffOptions): Promise { return withTestBackend(tauri.invoke, "get_diff", { rid, options, }); } export const cachedGetDiff = cached( getDiff, (...[rid, options]) => `get_diff:${rid}:${JSON.stringify(options, Object.keys(options).sort())}`, { max: 10_000 }, ); async function listCommits( rid: string, base: string, head: string, ): Promise { return withTestBackend(tauri.invoke, "list_commits", { rid, base, head }); } export const cachedListCommits = cached( listCommits, (...[rid, base, head]) => `list_commits:${rid}:${base}:${head}`, { max: 5_000 }, ); async function repoCommitCount(rid: string, head: string): Promise { return withTestBackend(tauri.invoke, "repo_commit_count", { rid, head }); } export const cachedRepoCommitCount = cached( repoCommitCount, (...[rid, head]) => `repo_commit_count:${rid}:${head}`, { max: 5_000 }, ); async function listJobs(rid: string, sha: string): Promise { return withTestBackend(tauri.invoke, "list_jobs", { rid, sha }); } // Listing jobs scans every job COB in the repo, so it is slow (hundreds of ms // to seconds). It is re-requested on every JobCob mount; caching by (rid, sha) // collapses concurrent identical calls into one in-flight request and reuses // the result across navigations. A short TTL keeps CI status reasonably fresh. export const cachedListJobs = cached( listJobs, (...[rid, sha]) => `list_jobs:${rid}:${sha}`, { max: 200, ttl: 30_000 }, ); async function diffStats( rid: string, base: string, head: string, ): Promise { return withTestBackend(tauri.invoke, "diff_stats", { rid, base, head, }); } export const cachedDiffStats = cached( diffStats, (...[rid, base, head]) => `diff_stats:${rid}:${base}:${head}`, { max: 10_000 }, ); // Patch text is generated by libgit2 on the backend (byte-accurate `git diff` // output) instead of being reconstructed from the structured diff model. // Callers pass the same `unified` value the rendered diff used so the text // matches what's on screen; `base` unset diffs `head` against its first // parent; `path` limits output to a single file. export async function getDiffText( rid: string, base: string | undefined, head: string, unified: number, path?: string, ): Promise { return withTestBackend(tauri.invoke, "get_diff_text", { rid, base, head, unified, path, }); } // Commits and patch revisions are immutable, so the same (base, head, unified, // path) always yields the same patch text — safe to cache. Capped since a // commit's full patch can be sizeable. export const cachedGetDiffText = cached( getDiffText, (...[rid, base, head, unified, path]) => `get_diff_text:${rid}:${base}:${head}:${unified}:${path}`, { max: 100 }, ); async function getCommitDiff( rid: string, sha: string, unified = 3, highlight = true, ): Promise { return withTestBackend(tauri.invoke, "get_commit_diff", { rid, sha, unified, highlight, }); } // Commits are immutable, so SHA is a perfect cache key. Cap entries since // each highlighted Diff can be sizeable. export const cachedGetCommitDiff = cached( getCommitDiff, (...[rid, sha, unified, highlight]) => `get_commit_diff:${rid}:${sha}:${unified}:${highlight}`, { max: 100 }, ); /** * Enumerating repos loads and verifies the identity doc of every repo in * storage, which takes ~100ms with many repos. The list rarely changes, so it * is cached with a short TTL; mutations that change it (adding, seeding, * unseeding) go through invalidateReposSummary(). */ export const cachedListReposSummary = cached( () => invoke("list_repos_summary"), () => "list_repos_summary", { max: 1, ttl: 30_000 }, ); export function invalidateReposSummary(): void { cachedListReposSummary.clear(); } export async function writeToClipboard( text: string, opts?: { label?: string; }, ) { if (window.__TAURI_INTERNALS__) { await tauri.invoke("plugin:clipboard-manager|write_text", { label: opts?.label, text, }); } else { await navigator.clipboard.writeText(text); } } export async function saveToDisk(fileName: string, text: string) { if (window.__TAURI_INTERNALS__) { // The backend opens the native save dialog and writes the file. await tauri.invoke("save_diff_to_disk", { name: fileName, content: text }); } else { // No native dialog outside Tauri (test/http mode): trigger a download. const url = URL.createObjectURL(new Blob([text], { type: "text/plain" })); const anchor = document.createElement("a"); anchor.href = url; anchor.download = fileName; anchor.click(); URL.revokeObjectURL(url); } } export { InvokeError };