Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
radicle-desktop src lib invoke.ts
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 * 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> {
  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 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 },
);

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 },
);

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 { InvokeError };