feat(layoutCounters): cache counters per org/repo for 30s
+ 134
- 0
this avoids useless queries to the database

after 30s of caching, data is considered stale, next request will query
again, and cache for 30s.

new file
app/services/repository/getRepositoryCounters.ts
@@ -0,0 +1,84 @@
+// 1st-party
+import { InMemoryCacheAdapter } from "@ethicdevs/fastify-stream-react-views";
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// app
+import { RepositoryCountersDTO } from "../../types";
+// service
+import type { RepositoryServiceDeps } from "./types";
+
+const CACHE = new InMemoryCacheAdapter();
+const CACHE_TTL = 30 * 1000; // 30s before cache expires
+
+const makeGetRepositoryCounters: ServiceMethodFactory<
+  RepositoryServiceDeps,
+  [string, string],
+  Promise<RepositoryCountersDTO | null>
+> = ({ request }) => {
+  return async (orgSlug, repoSlug) => {
+    const cacheKey = `${orgSlug}/${repoSlug}/counters`;
+    const lastRefresh = await CACHE.get(
+      `${orgSlug}/${repoSlug}/counters/lastRefresh`,
+    );
+
+    let isFreshCache = (request.params as any)["refetch"] != null || false;
+
+    // when last refresh is more than 30s ago, bypass the cache.
+    if (
+      lastRefresh != null &&
+      new Date(lastRefresh).getTime() > Date.now() - CACHE_TTL
+    ) {
+      isFreshCache = true;
+    }
+
+    // when cache is fresh, return it.
+    if (isFreshCache === false && (await CACHE.has(cacheKey))) {
+      const value = await CACHE.get(cacheKey);
+      if (value) return JSON.parse(value);
+      return DEFAULT_COUNTERS;
+    }
+
+    // retrieve total pulls in repository (PRs targeting this repo)
+    const totalPulls = await request.prisma.pullRequest.count({
+      where: {
+        targetRepository: {
+          organization: { slug: orgSlug },
+          slug: repoSlug,
+        },
+      },
+    });
+
+    // for now provide pulls and zeros
+    const counters: RepositoryCountersDTO = {
+      files: 0,
+      forks: 0,
+      branches: 0,
+      tags: 0,
+      commits: 0,
+      pulls: totalPulls,
+      tests: 0,
+      builds: 0,
+      issues: 0,
+      apiRefSymbols: 0,
+      helpCenterNotifs: 0,
+    };
+
+    await CACHE.set(cacheKey, JSON.stringify(counters));
+    return counters;
+  };
+};
+
+const DEFAULT_COUNTERS: RepositoryCountersDTO = {
+  files: 0,
+  forks: 0,
+  branches: 0,
+  tags: 0,
+  commits: 0,
+  pulls: 0,
+  tests: 0,
+  builds: 0,
+  issues: 0,
+  apiRefSymbols: 0,
+  helpCenterNotifs: 0,
+};
+
+export default makeGetRepositoryCounters;

@@ -206,6 +206,11 @@ export type RepositoryWithParentAndForkedFromRepos =
 
 // Centralized counters DTO to be reused by SSR layout and API
 export interface RepositoryCountersDTO {
+  files?: number;
+  forks?: number;
+  branches?: number;
+  tags?: number;
+  commits?: number;
   pulls?: number;
   tests?: number;
   builds?: number;

new file
app/utils/server/loadRepositoryCounters.ts
@@ -0,0 +1,45 @@
+// 3rd-party
+import type {
+  ContextConfigDefault,
+  FastifyContextConfig,
+  preHandlerHookHandler,
+} from "fastify";
+// app
+import type { RepositoryCountersDTO } from "../../types";
+import { makeRepositoryService } from "../../services/repository";
+
+declare module "fastify" {
+  export interface FastifyContext<ContextConfig = ContextConfigDefault> {
+    config: FastifyContextConfig & ContextConfig;
+    layoutCounters: RepositoryCountersDTO;
+  }
+}
+
+export const loadRepositoryCounters: preHandlerHookHandler = async (
+  request,
+  reply,
+  done,
+) => {
+  const { orgSlug, repoSlug } = request.params as any;
+  if (orgSlug == null || repoSlug == null) {
+    reply.context.layoutCounters = {
+      files: 0,
+      forks: 0,
+      branches: 0,
+      tags: 0,
+      commits: 0,
+      pulls: 0,
+      tests: 0,
+      builds: 0,
+      issues: 0,
+      apiRefSymbols: 0,
+      helpCenterNotifs: 0,
+    };
+    done();
+    return;
+  }
+  const repoService = makeRepositoryService({ request });
+  const counters = await repoService.getRepositoryCounters(orgSlug, repoSlug);
+  reply.context.layoutCounters = counters;
+  done();
+};