Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Reduce request for patch view
Merged did:key:z6MkkfM3...sVz5 opened 1 year ago

Adds the same previousLoadedRoute strategy used in source views to individual patches.

Since we where re-fetching a lot of diffs on patch view load and in between tags, this patch adds a svelte store that stores those diffs in memory (indexed by base and head commit and RID) and avoids refetching them since they are immutable.

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http

👉 Preview 👉 Workflow runs 👉 Branch on GitHub

6 files changed +148 -35 5174cda6 98857582
modified package-lock.json
@@ -22,6 +22,7 @@
        "hast-util-to-dom": "^4.0.0",
        "hast-util-to-html": "^9.0.1",
        "lodash": "^4.17.21",
+
        "lru-cache": "^11.0.0",
        "marked": "^14.0.0",
        "marked-emoji": "^1.4.2",
        "marked-footnote": "^1.2.2",
@@ -3377,6 +3378,14 @@
        "get-func-name": "^2.0.1"
      }
    },
+
    "node_modules/lru-cache": {
+
      "version": "11.0.0",
+
      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
+
      "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
+
      "engines": {
+
        "node": "20 || >=22"
+
      }
+
    },
    "node_modules/magic-string": {
      "version": "0.30.11",
      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
modified package.json
@@ -68,6 +68,7 @@
    "hast-util-to-dom": "^4.0.0",
    "hast-util-to-html": "^9.0.1",
    "lodash": "^4.17.21",
+
    "lru-cache": "^11.0.0",
    "marked": "^14.0.0",
    "marked-emoji": "^1.4.2",
    "marked-footnote": "^1.2.2",
added src/lib/cache.ts
@@ -0,0 +1,21 @@
+
import { LRUCache } from "lru-cache";
+

+
export function cached<Args extends unknown[], V>(
+
  f: (...args: Args) => Promise<V>,
+
  makeKey: (...args: Args) => string,
+
  options?: LRUCache.Options<string, { value: V }, unknown>,
+
): (...args: Args) => Promise<V> {
+
  const cache = new LRUCache(options || { max: 500 });
+
  return async function (...args: Args): Promise<V> {
+
    const key = makeKey(...args);
+
    const cached = cache.get(key);
+

+
    if (cached === undefined) {
+
      const value = await f(...args);
+
      cache.set(key, { value });
+
      return value;
+
    } else {
+
      return cached.value;
+
    }
+
  };
+
}
modified src/views/projects/Cob/Revision.svelte
@@ -11,6 +11,7 @@

  import * as utils from "@app/lib/utils";
  import { HttpdClient } from "@http-client";
+
  import { cachedGetDiff } from "@app/views/projects/router";
  import { onMount } from "svelte";

  import CobCommitTeaser from "@app/views/projects/Cob/CobCommitTeaser.svelte";
@@ -104,7 +105,12 @@
  onMount(async () => {
    try {
      loading = true;
-
      response = await api.project.getDiff(projectId, fromCommit, revisionOid);
+
      response = await cachedGetDiff(
+
        api.baseUrl,
+
        projectId,
+
        fromCommit,
+
        revisionOid,
+
      );
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (err: any) {
      error = err;
modified src/views/projects/DiffStatBadgeLoader.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
  import type { BaseUrl, Patch, Revision } from "@http-client";

-
  import { HttpdClient } from "@http-client";
+
  import { cachedGetDiff } from "@app/views/projects/router";
  import { formatCommit } from "@app/lib/utils";

  import DiffStatBadge from "@app/components/DiffStatBadge.svelte";
@@ -12,17 +12,9 @@
  export let baseUrl: BaseUrl;
  export let patch: Patch;
  export let latestRevision: Revision;
-

-
  $: diffPromise = api.project.getDiff(
-
    projectId,
-
    latestRevision.base,
-
    latestRevision.oid,
-
  );
-

-
  const api = new HttpdClient(baseUrl);
</script>

-
{#await diffPromise}
+
{#await cachedGetDiff(baseUrl, projectId, latestRevision.base, latestRevision.oid)}
  <Loading small />
{:then { diff }}
  <Link
modified src/views/projects/router.ts
@@ -13,21 +13,24 @@ import type {
  DiffBlob,
  Issue,
  IssueState,
+
  Node,
  Patch,
  PatchState,
  Project,
  Remote,
+
  Revision,
  SeedingPolicy,
  Tree,
} from "@http-client";

import * as Syntax from "@app/lib/syntax";
import config from "virtual:config";
-
import { isLocal, unreachable } from "@app/lib/utils";
-
import { nodePath } from "@app/views/nodes/router";
-
import { handleError, unreachableError } from "@app/views/projects/error";
import { HttpdClient } from "@http-client";
import { ResponseError, ResponseParseError } from "@http-client/lib/fetcher";
+
import { cached } from "@app/lib/cache";
+
import { handleError, unreachableError } from "@app/views/projects/error";
+
import { isLocal, unreachable } from "@app/lib/utils";
+
import { nodePath } from "@app/views/nodes/router";

export const PATCHES_PER_PAGE = 10;
export const ISSUES_PER_PAGE = 10;
@@ -225,6 +228,15 @@ function isOid(input: string): boolean {
  return /^[a-fA-F0-9]{40}$/.test(input);
}

+
export const cachedGetDiff = cached(
+
  async (baseUrl: BaseUrl, rid: string, base: string, oid: string) => {
+
    const api = new HttpdClient(baseUrl);
+
    return await api.project.getDiff(rid, base, oid);
+
  },
+
  (...args) => JSON.stringify(args),
+
  { max: 200 },
+
);
+

function parseRevisionToOid(
  revision: string | undefined,
  defaultBranch: string,
@@ -269,7 +281,7 @@ export async function loadProjectRoute(
    if (route.resource === "project.source") {
      return await loadTreeView(route, previousLoaded);
    } else if (route.resource === "project.history") {
-
      return await loadHistoryView(route);
+
      return await loadHistoryView(route, previousLoaded);
    } else if (route.resource === "project.commit") {
      const [project, commit, seedingPolicy, node] = await Promise.all([
        api.project.getById(route.project),
@@ -291,7 +303,7 @@ export async function loadProjectRoute(
    } else if (route.resource === "project.issue") {
      return await loadIssueView(route);
    } else if (route.resource === "project.patch") {
-
      return await loadPatchView(route);
+
      return await loadPatchView(route, previousLoaded);
    } else if (route.resource === "project.issues") {
      return await loadIssuesView(route);
    } else if (route.resource === "project.patches") {
@@ -386,25 +398,31 @@ async function loadTreeView(
  let projectPromise: Promise<Project>;
  let seedingPolicyPromise: Promise<SeedingPolicy>;
  let peersPromise: Promise<Remote[]>;
+
  let nodePromise: Promise<Partial<Node>>;
  if (
-
    previousLoaded.resource === "project.source" &&
+
    (previousLoaded.resource === "project.source" ||
+
      previousLoaded.resource === "project.history") &&
    previousLoaded.params.project.id === route.project &&
    previousLoaded.params.peer === route.peer
  ) {
    projectPromise = Promise.resolve(previousLoaded.params.project);
    peersPromise = Promise.resolve(previousLoaded.params.peers);
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
+
    nodePromise = Promise.resolve({
+
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
+
    });
  } else {
    projectPromise = api.project.getById(route.project);
    peersPromise = api.project.getAllRemotes(route.project);
    seedingPolicyPromise = api.getPolicyById(route.project);
+
    nodePromise = api.getNode();
  }

  const [project, peers, seedingPolicy, node] = await Promise.all([
    projectPromise,
    peersPromise,
    seedingPolicyPromise,
-
    api.getNode(),
+
    nodePromise,
  ]);

  let branchMap: Record<string, string> = {
@@ -508,15 +526,39 @@ async function loadBlob(
}
async function loadHistoryView(
  route: ProjectHistoryRoute,
+
  previousLoaded: LoadedRoute,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);

+
  let projectPromise: Promise<Project>;
+
  let seedingPolicyPromise: Promise<SeedingPolicy>;
+
  let peersPromise: Promise<Remote[]>;
+
  let nodePromise: Promise<Partial<Node>>;
+
  if (
+
    (previousLoaded.resource === "project.source" ||
+
      previousLoaded.resource === "project.history") &&
+
    previousLoaded.params.project.id === route.project &&
+
    previousLoaded.params.peer === route.peer
+
  ) {
+
    projectPromise = Promise.resolve(previousLoaded.params.project);
+
    peersPromise = Promise.resolve(previousLoaded.params.peers);
+
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
+
    nodePromise = Promise.resolve({
+
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
+
    });
+
  } else {
+
    projectPromise = api.project.getById(route.project);
+
    peersPromise = api.project.getAllRemotes(route.project);
+
    seedingPolicyPromise = api.getPolicyById(route.project);
+
    nodePromise = api.getNode();
+
  }
+

  const [project, peers, seedingPolicy, branchMap, node] = await Promise.all([
-
    api.project.getById(route.project),
-
    api.project.getAllRemotes(route.project),
-
    api.getPolicyById(route.project),
+
    projectPromise,
+
    peersPromise,
+
    seedingPolicyPromise,
    getPeerBranches(api, route.project, route.peer),
-
    api.getNode(),
+
    nodePromise,
  ]);

  let commitId;
@@ -534,9 +576,22 @@ async function loadHistoryView(
    );
  }

+
  let treePromise: Promise<Tree>;
+

+
  if (
+
    (previousLoaded.resource === "project.source" ||
+
      previousLoaded.resource === "project.history") &&
+
    previousLoaded.params.project.id === route.project &&
+
    previousLoaded.params.commit === commitId
+
  ) {
+
    treePromise = Promise.resolve(previousLoaded.params.tree);
+
  } else {
+
    treePromise = api.project.getTree(route.project, commitId);
+
  }
+

  const [tree, commitHeaders] = await Promise.all([
-
    api.project.getTree(route.project, commitId),
-
    await api.project.getAllCommits(project.id, {
+
    treePromise,
+
    api.project.getAllCommits(project.id, {
      parent: commitId,
      page: 0,
      perPage: config.source.commitsPerPage,
@@ -590,6 +645,7 @@ async function loadIssueView(

async function loadPatchView(
  route: ProjectPatchRoute,
+
  previousLoaded: LoadedRoute,
): Promise<ProjectLoadedRoute> {
  const api = new HttpdClient(route.node);
  const rawPath = (commit?: string) =>
@@ -597,14 +653,40 @@ async function loadPatchView(
      route.project
    }${commit ? `/${commit}` : ""}`;

-
  const [project, patch, seedingPolicy, node] = await Promise.all([
-
    api.project.getById(route.project),
-
    api.project.getPatchById(route.project, route.patch),
-
    api.getPolicyById(route.project),
-
    api.getNode(),
+
  let projectPromise: Promise<Project>;
+
  let patchPromise: Promise<Patch>;
+
  let nodePromise: Promise<Partial<Node>>;
+
  let seedingPolicyPromise: Promise<SeedingPolicy>;
+

+
  if (
+
    previousLoaded.resource === "project.patch" &&
+
    previousLoaded.params.project.id === route.project &&
+
    previousLoaded.params.patch.id === route.patch
+
  ) {
+
    projectPromise = Promise.resolve(previousLoaded.params.project);
+
    patchPromise = Promise.resolve(previousLoaded.params.patch);
+
    seedingPolicyPromise = Promise.resolve(previousLoaded.params.seedingPolicy);
+
    nodePromise = Promise.resolve({
+
      avatarUrl: previousLoaded.params.nodeAvatarUrl,
+
    });
+
  } else {
+
    projectPromise = api.project.getById(route.project);
+
    patchPromise = api.project.getPatchById(route.project, route.patch);
+
    seedingPolicyPromise = api.getPolicyById(route.project);
+
    nodePromise = api.getNode();
+
  }
+
  const [project, patch, seedingPolicy, { avatarUrl }] = await Promise.all([
+
    projectPromise,
+
    patchPromise,
+
    seedingPolicyPromise,
+
    nodePromise,
  ]);
-
  const latestRevision = patch.revisions[patch.revisions.length - 1];
-
  const { diff } = await api.project.getDiff(
+

+
  const latestRevision = patch.revisions.at(-1) as Revision;
+
  const {
+
    diff: { stats },
+
  } = await cachedGetDiff(
+
    api.baseUrl,
    route.project,
    latestRevision.base,
    latestRevision.oid,
@@ -626,7 +708,8 @@ async function loadPatchView(
          `revision ${revisionId} of patch ${route.patch} not found`,
        );
      }
-
      const { diff, commits, files } = await api.project.getDiff(
+
      const { diff, commits, files } = await cachedGetDiff(
+
        api.baseUrl,
        route.project,
        revision.base,
        revision.oid,
@@ -643,7 +726,8 @@ async function loadPatchView(
    }
    case "diff": {
      const { fromCommit, toCommit } = route.view;
-
      const { diff, files } = await api.project.getDiff(
+
      const { diff, files } = await cachedGetDiff(
+
        api.baseUrl,
        route.project,
        fromCommit,
        toCommit,
@@ -661,9 +745,9 @@ async function loadPatchView(
      project,
      rawPath,
      patch,
-
      stats: diff.stats,
+
      stats,
      view,
-
      nodeAvatarUrl: node.avatarUrl,
+
      nodeAvatarUrl: avatarUrl,
    },
  };
}