Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Consolidate and optimize data fetching for ProjectCard
Merged did:key:z6Mki9XN...FvWF opened 2 years ago

We consolidate the code that fetches the data for ProjectCard in one module, adjacent to the ProjectCard component. We also consolidate all the data into one interface to simplify the component API.

With the data loading in one place we can eliminate duplicate code and a handful of helper functions.

The consolidation also allows us to discover some optimizations to fetching: We parallelize requests and we use the commit detail endpoint instead of the commit list endpoint. For the latter to pay off, we need to deploy caching to the API.

7 files changed +94 -147 d315380e 67e16ea7
modified src/components/ProjectCard.svelte
@@ -1,32 +1,25 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
-

-
  import type { WeeklyActivity } from "@app/lib/commit";
  import { formatTimestamp, twemoji } from "@app/lib/utils";

  import ActivityDiagram from "@app/components/ActivityDiagram.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Link from "@app/components/Link.svelte";

-
  export let compact = false;
+
  import type { ProjectInfo } from "./ProjectCard";

-
  export let activity: WeeklyActivity[];
-
  export let description: string;
-
  export let baseUrl: BaseUrl;
+
  export let compact = false;

-
  export let numberOfIssues: number;
-
  export let numberOfPatches: number;
+
  export let projectInfo: ProjectInfo;

  export let isDelegate: boolean;
  export let isSeeding: boolean;
-
  export let isPrivate: boolean;
-

-
  export let lastUpdatedTimestamp: number;
-

-
  $: lastUpdated = formatTimestamp(lastUpdatedTimestamp);

-
  export let id: string;
-
  export let name: string;
+
  $: project = projectInfo.project;
+
  $: baseUrl = projectInfo.baseUrl;
+
  $: isPrivate = project.visibility?.type === "private";
+
  $: lastUpdated = formatTimestamp(
+
    projectInfo.lastCommit.commit.committer.time,
+
  );
</script>

<style>
@@ -138,21 +131,21 @@
<Link
  route={{
    resource: "project.source",
-
    project: id,
+
    project: project.id,
    node: baseUrl,
  }}>
  <div class="project-card" class:compact>
    <div class="activity">
      <div class="fadeout-overlay" />
      <ActivityDiagram
-
        {id}
+
        id={project.id}
        viewBoxHeight={200}
        styleColor="var(--color-foreground-primary"
-
        {activity} />
+
        activity={projectInfo.activity} />
    </div>
    <div class="title">
      <div class="headline-and-badges">
-
        <h4 use:twemoji>{name}</h4>
+
        <h4 use:twemoji>{project.name}</h4>
        <div class="badges">
          {#if isPrivate}
            <div
@@ -180,14 +173,14 @@
          {/if}
        </div>
      </div>
-
      <p class="txt-small" use:twemoji>{description}</p>
+
      <p class="txt-small" use:twemoji>{project.description}</p>
    </div>
    <div class="stats-row txt-tiny" style:color="var(--color-foreground-dim)">
      <IconSmall name="issue" />
-
      {numberOfIssues} ·
+
      {project.issues.open} ·
      <IconSmall name="patch" />
      <span style:overflow="hidden" style:text-overflow="ellipsis">
-
        {numberOfPatches} · Updated {lastUpdated}
+
        {project.patches.open} · Updated {lastUpdated}
      </span>
    </div>
  </div>
added src/components/ProjectCard.ts
@@ -0,0 +1,38 @@
+
import { loadProjectActivity, type WeeklyActivity } from "@app/lib/commit";
+
import {
+
  HttpdClient,
+
  type BaseUrl,
+
  type Commit,
+
  type Project,
+
} from "@httpd-client";
+

+
export interface ProjectInfo {
+
  project: Project;
+
  baseUrl: BaseUrl;
+
  activity: WeeklyActivity[];
+
  lastCommit: Commit;
+
}
+

+
export async function fetchProjectInfos(
+
  baseUrl: BaseUrl,
+
  show: "all" | "pinned",
+
): Promise<ProjectInfo[]> {
+
  const api = new HttpdClient(baseUrl);
+
  const projects = await api.project.getAll({ show });
+
  const info = await Promise.all(
+
    projects.map(async project => {
+
      const [activity, lastCommit] = await Promise.all([
+
        loadProjectActivity(project.id, baseUrl),
+
        api.project.getCommitBySha(project.id, project.head),
+
      ]);
+
      return { project, activity, lastCommit, baseUrl };
+
    }),
+
  );
+

+
  return info.sort((a, b) => {
+
    const aLastCommit = a.lastCommit.commit.committer.time;
+
    const bLastCommit = b.lastCommit.commit.committer.time;
+

+
    return bLastCommit - aLastCommit;
+
  });
+
}
modified src/lib/commit.ts
@@ -116,10 +116,3 @@ export async function loadProjectActivity(id: string, baseUrl: BaseUrl) {

  return groupCommitsByWeek(commits.activity);
}
-

-
export async function fetchLastCommit(id: string, baseUrl: BaseUrl) {
-
  const api = new HttpdClient(baseUrl);
-
  const res = await api.project.getAllCommits(id, { perPage: 1 });
-

-
  return res.commits[0];
-
}
modified src/lib/projects.ts
@@ -2,22 +2,12 @@ import type { BaseUrl, Project } from "@httpd-client";

import { HttpdClient } from "@httpd-client";
import { isFulfilled } from "@app/lib/utils";
-
import {
-
  fetchLastCommit,
-
  loadProjectActivity,
-
  type WeeklyActivity,
-
} from "./commit";

export interface ProjectBaseUrl {
  project: Project;
  baseUrl: BaseUrl;
}

-
export interface ProjectWithListingData extends ProjectBaseUrl {
-
  activity: WeeklyActivity[];
-
  lastCommit: Awaited<ReturnType<typeof fetchLastCommit>>;
-
}
-

export async function getProjectsFromNodes(
  params: { id: string; baseUrl: BaseUrl }[],
): Promise<ProjectBaseUrl[]> {
@@ -34,32 +24,6 @@ export async function getProjectsFromNodes(
  return results.filter(isFulfilled).map(r => r.value);
}

-
export async function getProjectListingData(id: string, baseUrl: BaseUrl) {
-
  const activity = await loadProjectActivity(id, baseUrl);
-
  const lastCommit = await fetchLastCommit(id, baseUrl);
-

-
  return { activity, lastCommit };
-
}
-

-
export async function getProjectsListingData(projects: ProjectBaseUrl[]) {
-
  const result = await Promise.all(
-
    projects.map(async ({ project, baseUrl }) => {
-
      const { activity, lastCommit } = await getProjectListingData(
-
        project.id,
-
        baseUrl,
-
      );
-
      return { project, activity, lastCommit, baseUrl };
-
    }),
-
  );
-

-
  return result.sort((a, b) => {
-
    const aLastCommit = a.lastCommit?.commit.committer.time ?? 0;
-
    const bLastCommit = b.lastCommit?.commit.committer.time ?? 0;
-

-
    return bLastCommit - aLastCommit;
-
  });
-
}
-

export async function queryProject(
  baseUrl: BaseUrl,
  projectId: string,
modified src/views/home/Index.svelte
@@ -1,17 +1,13 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
-
  import type { ProjectWithListingData } from "@app/lib/projects";
  import type { ComponentProps } from "svelte";

  import storedWritable from "@efstajas/svelte-stored-writable";
-
  import { HttpdClient } from "@httpd-client";
  import { derived } from "svelte/store";
  import { literal, union } from "zod";

  import { api, httpdStore } from "@app/lib/httpd";
  import { deduplicateStore } from "@app/lib/deduplicateStore";
  import { baseUrlToString } from "@app/lib/utils";
-
  import { getProjectsListingData } from "@app/lib/projects";
  import { handleError } from "@app/views/home/error";
  import { isDelegate } from "@app/lib/roles";
  import { preferredSeeds } from "@app/lib/seeds";
@@ -25,6 +21,10 @@
  import HomepageSection from "./components/HomepageSection.svelte";
  import NewProjectButton from "./components/NewProjectButton.svelte";
  import PreferredSeedDropdown from "./components/PreferredSeedDropdown.svelte";
+
  import {
+
    fetchProjectInfos,
+
    type ProjectInfo,
+
  } from "@app/components/ProjectCard";

  const selectedSeed = deduplicateStore(
    derived(preferredSeeds, $ => $?.selected),
@@ -40,30 +40,17 @@
  );

  let localProjects:
-
    | ProjectWithListingData[]
+
    | ProjectInfo[]
    | ComponentProps<ErrorMessage>["error"]
    | undefined;
  let preferredSeedProjects:
-
    | ProjectWithListingData[]
+
    | ProjectInfo[]
    | ComponentProps<ErrorMessage>["error"]
    | undefined;

-
  async function fetchProjects(baseUrl: BaseUrl, show: "all" | "pinned") {
-
    const api = new HttpdClient(baseUrl);
-

-
    const projects = (await api.project.getAll({ perPage: 30, show })).map(
-
      project => ({
-
        project,
-
        baseUrl,
-
      }),
-
    );
-

-
    return await getProjectsListingData(projects);
-
  }
-

  async function loadLocalProjects() {
    localProjects = undefined;
-
    localProjects = await fetchProjects(api.baseUrl, "all").catch(
+
    localProjects = await fetchProjectInfos(api.baseUrl, "all").catch(
      error => error,
    );
  }
@@ -72,9 +59,10 @@
    preferredSeedProjects = undefined;

    if (!$selectedSeed) return;
-
    preferredSeedProjects = await fetchProjects($selectedSeed, "pinned").catch(
-
      error => error,
-
    );
+
    preferredSeedProjects = await fetchProjectInfos(
+
      $selectedSeed,
+
      "pinned",
+
    ).catch(error => error);
  }

  function isSeeding(projectId: string) {
@@ -176,19 +164,12 @@
        </svelte:fragment>
        <div class="project-grid">
          {#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
-
            {#each filteredLocalProjects as { project, baseUrl, activity, lastCommit }}
+
            {#each filteredLocalProjects as projectInfo}
              <ProjectCard
-
                id={project.id}
-
                name={project.name}
-
                description={project.description}
-
                numberOfIssues={project.issues.open}
-
                numberOfPatches={project.patches.open}
-
                isPrivate={project.visibility?.type === "private"}
+
                {projectInfo}
                isSeeding={true}
-
                isDelegate={isDelegate(nodeId, project.delegates) ?? false}
-
                lastUpdatedTimestamp={lastCommit.commit.committer.time}
-
                {activity}
-
                {baseUrl} />
+
                isDelegate={isDelegate(nodeId, projectInfo.project.delegates) ??
+
                  false} />
            {/each}
          {/if}
        </div>
@@ -228,19 +209,12 @@
      </svelte:fragment>
      <div class="project-grid">
        {#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
-
          {#each preferredSeedProjects as { project, baseUrl, activity, lastCommit }}
+
          {#each preferredSeedProjects as projectInfo}
            <ProjectCard
-
              id={project.id}
-
              name={project.name}
-
              description={project.description}
-
              numberOfIssues={project.issues.open}
-
              numberOfPatches={project.patches.open}
-
              isPrivate={project.visibility?.type === "private"}
-
              isSeeding={isSeeding(project.id)}
-
              isDelegate={isDelegate(nodeId, project.delegates) ?? false}
-
              lastUpdatedTimestamp={lastCommit.commit.committer.time}
-
              {activity}
-
              {baseUrl} />
+
              {projectInfo}
+
              isSeeding={isSeeding(projectInfo.project.id)}
+
              isDelegate={isDelegate(nodeId, projectInfo.project.delegates) ??
+
                false} />
          {/each}
        {/if}
      </div>
modified src/views/nodes/View.svelte
@@ -1,6 +1,5 @@
<script lang="ts">
  import type { BaseUrl, Policy, Scope } from "@httpd-client";
-
  import type { ProjectWithListingData } from "@app/lib/projects";

  import capitalize from "lodash/capitalize";

@@ -14,11 +13,12 @@
  import ProjectCard from "@app/components/ProjectCard.svelte";
  import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
  import HoverPopover from "@app/components/HoverPopover.svelte";
+
  import type { ProjectInfo } from "@app/components/ProjectCard";

  export let baseUrl: BaseUrl;
  export let nid: string;
  export let externalAddresses: string[];
-
  export let projects: ProjectWithListingData[] = [];
+
  export let projectInfos: ProjectInfo[];
  export let version: string;
  export let policy: Policy | undefined = undefined;
  export let scope: Scope | undefined = undefined;
@@ -135,7 +135,7 @@

      <div class="subtitle">
        <div class="pinned txt-semibold">
-
          {projects.length}
+
          {projectInfos.length}
          {isLocal(baseUrl.hostname) ? "" : "pinned"} projects
        </div>

@@ -188,20 +188,14 @@
      </div>

      <div class="project-grid">
-
        {#each projects as { project, baseUrl, activity, lastCommit }}
+
        {#each projectInfos as projectInfo}
          <ProjectCard
-
            id={project.id}
-
            name={project.name}
-
            description={project.description}
-
            numberOfIssues={project.issues.open}
-
            numberOfPatches={project.patches.open}
-
            isPrivate={project.visibility?.type === "private"}
+
            {projectInfo}
            isSeeding={false}
-
            isDelegate={isDelegate(session?.publicKey, project.delegates) ??
-
              false}
-
            lastUpdatedTimestamp={lastCommit.commit.committer.time}
-
            {activity}
-
            {baseUrl} />
+
            isDelegate={isDelegate(
+
              session?.publicKey,
+
              projectInfo.project.delegates,
+
            ) ?? false} />
        {/each}
      </div>
    </div>
modified src/views/nodes/router.ts
@@ -1,12 +1,14 @@
import type { BaseUrl, Policy, Scope } from "@httpd-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
-
import type { ProjectWithListingData } from "@app/lib/projects";

import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
import { baseUrlToString, isLocal } from "@app/lib/utils";
-
import { getProjectsListingData } from "@app/lib/projects";
import { handleError } from "@app/views/nodes/error";
+
import {
+
  fetchProjectInfos,
+
  type ProjectInfo,
+
} from "@app/components/ProjectCard";

export interface NodesRouteParams {
  baseUrl: BaseUrl;
@@ -25,26 +27,12 @@ export interface NodesLoadedRoute {
    version: string;
    externalAddresses: string[];
    nid: string;
-
    projects: ProjectWithListingData[];
+
    projectInfos: ProjectInfo[];
    policy?: Policy;
    scope?: Scope;
  };
}

-
async function loadProjects(
-
  baseUrl: BaseUrl,
-
): Promise<ProjectWithListingData[]> {
-
  const api = new HttpdClient(baseUrl);
-
  const projects = await api.project.getAll({
-
    show: isLocal(baseUrl.hostname) ? "all" : "pinned",
-
  });
-
  const results = await getProjectsListingData(
-
    projects.map(p => ({ project: p, baseUrl })),
-
  );
-

-
  return results;
-
}
-

export function nodePath(baseUrl: BaseUrl) {
  const port = baseUrl.port ?? config.nodes.defaultHttpdPort;

@@ -60,9 +48,12 @@ export async function loadNodeRoute(
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
  const api = new HttpdClient(params.baseUrl);
  try {
-
    const [node, projects] = await Promise.all([
+
    const [node, projectInfos] = await Promise.all([
      api.getNode(),
-
      loadProjects(params.baseUrl),
+
      fetchProjectInfos(
+
        params.baseUrl,
+
        isLocal(params.baseUrl.hostname) ? "all" : "pinned",
+
      ),
    ]);
    return {
      resource: "nodes",
@@ -71,7 +62,7 @@ export async function loadNodeRoute(
        nid: node.id,
        externalAddresses: node.config?.externalAddresses ?? [],
        version: node.version,
-
        projects: projects,
+
        projectInfos: projectInfos,
        policy: node.config?.policy,
        scope: node.config?.scope,
      },