mirror of
https://seed.radicle.dev/rad:z4D5UCArafTzTQpDZNQRuqswh3ury.git
synced 2026-11-07 12:20:45 +08:00
262 lines
7.3 KiB
TypeScript
262 lines
7.3 KiB
TypeScript
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<T = null>(
|
|
cmd: string,
|
|
args?: tauri.InvokeArgs,
|
|
options?: tauri.InvokeOptions,
|
|
): Promise<T> {
|
|
if (import.meta.env.DEV) {
|
|
const start = performance.now();
|
|
try {
|
|
return await withTestBackend<T>(tauri.invoke, cmd, args, options);
|
|
} finally {
|
|
const ms = Math.round(performance.now() - start);
|
|
|
|
console.debug(`[invoke] ${cmd} ${ms}ms`);
|
|
}
|
|
}
|
|
return withTestBackend<T>(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<T>(
|
|
fn: (
|
|
cmd: string,
|
|
args?: tauri.InvokeArgs,
|
|
options?: tauri.InvokeOptions,
|
|
) => Promise<T>,
|
|
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<Diff> {
|
|
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<Commit[]> {
|
|
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<number> {
|
|
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<Job[]> {
|
|
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<Stats> {
|
|
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<string> {
|
|
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<Diff> {
|
|
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<RepoSummary[]>("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 };
|