Skip to content

Commit e894585

Browse files
refactor: abstract cached data fetching, remove useless functions
1 parent d29130f commit e894585

File tree

1 file changed

+128
-147
lines changed

1 file changed

+128
-147
lines changed

src/lib/server/github-cache.ts

Lines changed: 128 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,50 @@ export class GitHubCache {
115115
return `repo:${owner}/${repo}:${type}${strArgs}`;
116116
}
117117

118+
/**
119+
* An abstraction over general processing that:
120+
* 1. tries getting stuff from Redis cache
121+
* 2. calls the promise to get new data if no value is found in cache
122+
* 3. store this new value back in the cache with an optional TTL before returning the value.
123+
*
124+
* @private
125+
*/
126+
#processCached<RType extends Parameters<InstanceType<typeof Redis>["json"]["set"]>[2]>() {
127+
/**
128+
* Inner currying function to circumvent unsupported partial inference
129+
*
130+
* @param cacheKey the cache key to fetch Redis with
131+
* @param promise the promise to call to get new data if the cache is empty
132+
* @param transformer the function that transforms the return from the promise to the target return value
133+
* @param ttl the optional TTL to use for the newly cached data
134+
*
135+
* @see {@link https://github.com/microsoft/TypeScript/issues/26242|Partial type inference discussion}
136+
*/
137+
return async <PromiseType>(
138+
cacheKey: string,
139+
promise: () => Promise<PromiseType>,
140+
transformer: (from: Awaited<PromiseType>) => RType | Promise<RType>,
141+
ttl: number | undefined = undefined
142+
): Promise<RType> => {
143+
const cachedValue = await this.#redis.json.get<RType>(cacheKey);
144+
if (cachedValue) {
145+
console.log(`Cache hit for ${cacheKey}`);
146+
return cachedValue;
147+
}
148+
149+
console.log(`Cache miss for ${cacheKey}`);
150+
151+
const newValue = await transformer(await promise());
152+
153+
await this.#redis.json.set(cacheKey, "$", newValue);
154+
if (ttl !== undefined) {
155+
await this.#redis.expire(cacheKey, ttl);
156+
}
157+
158+
return newValue;
159+
};
160+
}
161+
118162
/**
119163
* Get the item (issue or pr) with the given information.
120164
* Return the appropriate value if the type is defined or
@@ -167,28 +211,21 @@ export class GitHubCache {
167211
* @throws Error if the issue is not found
168212
*/
169213
async getIssueDetails(owner: string, repo: string, id: number) {
170-
const cacheKey = this.#getRepoKey(owner, repo, "issue", id);
171-
172-
const cachedDetails = await this.#redis.json.get<IssueDetails>(cacheKey);
173-
if (cachedDetails) {
174-
console.log(`Cache hit for issue details for ${cacheKey}`);
175-
return cachedDetails;
176-
}
177-
178-
console.log(`Cache miss for issue details for ${cacheKey}, fetching from the GitHub API`);
179-
180-
const [{ data: info }, { data: comments }, linkedPrs] = await Promise.all([
181-
this.#octokit.rest.issues.get({ owner, repo, issue_number: id }),
182-
this.#octokit.rest.issues.listComments({ owner, repo, issue_number: id }),
183-
this.#getLinkedPullRequests(owner, repo, id)
184-
]);
185-
186-
const details: IssueDetails = { info, comments, linkedPrs };
187-
188-
await this.#redis.json.set(cacheKey, "$", details);
189-
await this.#redis.expire(cacheKey, FULL_DETAILS_TTL);
190-
191-
return details;
214+
return await this.#processCached<IssueDetails>()(
215+
this.#getRepoKey(owner, repo, "issue", id),
216+
() =>
217+
Promise.all([
218+
this.#octokit.rest.issues.get({ owner, repo, issue_number: id }),
219+
this.#octokit.rest.issues.listComments({ owner, repo, issue_number: id }),
220+
this.#getLinkedPullRequests(owner, repo, id)
221+
]),
222+
([{ data: info }, { data: comments }, linkedPrs]) => ({
223+
info,
224+
comments,
225+
linkedPrs
226+
}),
227+
FULL_DETAILS_TTL
228+
);
192229
}
193230

194231
/**
@@ -201,32 +238,25 @@ export class GitHubCache {
201238
* @throws Error if the PR is not found
202239
*/
203240
async getPullRequestDetails(owner: string, repo: string, id: number) {
204-
const cacheKey = this.#getRepoKey(owner, repo, "pr", id);
205-
206-
const cachedDetails = await this.#redis.json.get<PullRequestDetails>(cacheKey);
207-
if (cachedDetails) {
208-
console.log(`Cache hit for PR details for ${cacheKey}`);
209-
return cachedDetails;
210-
}
211-
212-
console.log(`Cache miss for PR details for ${id}, fetching from the GitHub API`);
213-
214-
const [{ data: info }, { data: comments }, { data: commits }, { data: files }, linkedIssues] =
215-
await Promise.all([
216-
this.#octokit.rest.pulls.get({ owner, repo, pull_number: id }),
217-
this.#octokit.rest.issues.listComments({ owner, repo, issue_number: id }),
218-
this.#octokit.rest.pulls.listCommits({ owner, repo, pull_number: id }),
219-
this.#octokit.rest.pulls.listFiles({ owner, repo, pull_number: id }),
220-
this.#getLinkedIssues(owner, repo, id)
221-
]);
222-
223-
const details: PullRequestDetails = { info, comments, commits, files, linkedIssues };
224-
225-
// Cache the result
226-
await this.#redis.json.set(cacheKey, "$", details);
227-
await this.#redis.expire(cacheKey, FULL_DETAILS_TTL);
228-
229-
return details;
241+
return await this.#processCached<PullRequestDetails>()(
242+
this.#getRepoKey(owner, repo, "pr", id),
243+
() =>
244+
Promise.all([
245+
this.#octokit.rest.pulls.get({ owner, repo, pull_number: id }),
246+
this.#octokit.rest.issues.listComments({ owner, repo, issue_number: id }),
247+
this.#octokit.rest.pulls.listCommits({ owner, repo, pull_number: id }),
248+
this.#octokit.rest.pulls.listFiles({ owner, repo, pull_number: id }),
249+
this.#getLinkedIssues(owner, repo, id)
250+
]),
251+
([{ data: info }, { data: comments }, { data: commits }, { data: files }, linkedIssues]) => ({
252+
info,
253+
comments,
254+
commits,
255+
files,
256+
linkedIssues
257+
}),
258+
FULL_DETAILS_TTL
259+
);
230260
}
231261

232262
/**
@@ -364,22 +394,12 @@ export class GitHubCache {
364394
* @returns the releases, either cached or fetched
365395
*/
366396
async getReleases(repository: Repository) {
367-
const cacheKey = this.#getRepoKey(repository.owner, repository.repoName, "releases");
368-
369-
const cachedReleases = await this.#redis.json.get<GitHubRelease[]>(cacheKey);
370-
if (cachedReleases) {
371-
console.log(`Cache hit for releases for ${cacheKey}`);
372-
return cachedReleases;
373-
}
374-
375-
console.log(`Cache miss for releases for ${cacheKey}, fetching from GitHub API`);
376-
377-
const releases = await this.#fetchReleases(repository);
378-
379-
await this.#redis.json.set(cacheKey, "$", releases);
380-
await this.#redis.expire(cacheKey, RELEASES_TTL);
381-
382-
return releases;
397+
return await this.#processCached<GitHubRelease[]>()(
398+
this.#getRepoKey(repository.owner, repository.repoName, "releases"),
399+
() => this.#fetchReleases(repository),
400+
releases => releases,
401+
RELEASES_TTL
402+
);
383403
}
384404

385405
/**
@@ -520,90 +540,51 @@ export class GitHubCache {
520540
* @private
521541
*/
522542
async getDescriptions(owner: string, repo: string) {
523-
const cacheKey = this.#getRepoKey(owner, repo, "descriptions");
524-
525-
const cachedDescriptions = await this.#redis.json.get<{ [key: string]: string }>(cacheKey);
526-
if (cachedDescriptions) {
527-
console.log(`Cache hit for descriptions for ${cacheKey}`);
528-
return cachedDescriptions;
529-
}
530-
531-
console.log(`Cache miss for releases for ${cacheKey}, fetching from GitHub API`);
532-
533-
const { data: allFiles } = await this.#octokit.rest.git.getTree({
534-
owner,
535-
repo,
536-
tree_sha: "HEAD",
537-
recursive: "true"
538-
});
539-
540-
const allPackageJson = allFiles.tree
541-
.map(({ path }) => path)
542-
.filter(path => path !== undefined)
543-
.filter(
544-
path =>
545-
!path.includes("/test/") &&
546-
!path.includes("/e2e-tests/") &&
547-
(path === "package.json" || path.endsWith("/package.json"))
548-
);
549-
550-
const descriptions = new Map<string, string>();
551-
for (const path of allPackageJson) {
552-
const { data: packageJson } = await this.#octokit.rest.repos.getContent({
553-
owner,
554-
repo,
555-
path
556-
});
557-
558-
if (!("content" in packageJson)) continue; // filter out empty or multiple results
559-
const { content, encoding, type } = packageJson;
560-
if (type !== "file" || !content) continue; // filter out directories and empty files
561-
const packageFile =
562-
encoding === "base64" ? Buffer.from(content, "base64").toString() : content;
563-
564-
try {
565-
const { description } = JSON.parse(packageFile) as { description: string };
566-
if (description) descriptions.set(path, description);
567-
} catch {
568-
// ignore
569-
}
570-
}
571-
572-
await this.#redis.json.set(cacheKey, "$", Object.fromEntries(descriptions));
573-
await this.#redis.expire(cacheKey, DESCRIPTIONS_TTL);
574-
575-
return Object.fromEntries(descriptions);
576-
}
577-
578-
/**
579-
* Checks if releases are present in the cache for the
580-
* given GitHub info
581-
*
582-
* @param owner the owner of the GitHub repository to check the
583-
* existence in the cache for
584-
* @param repo the name of the GitHub repository to check the
585-
* existence in the cache for
586-
* @param type the kind of cache to target
587-
* @returns whether the repository is cached or not
588-
*/
589-
async exists(owner: string, repo: string, type: KeyType) {
590-
const cacheKey = this.#getRepoKey(owner, repo, type);
591-
const result = await this.#redis.exists(cacheKey);
592-
return result === 1;
593-
}
543+
return await this.#processCached<{ [key: string]: string }>()(
544+
this.#getRepoKey(owner, repo, "descriptions"),
545+
() =>
546+
this.#octokit.rest.git.getTree({
547+
owner,
548+
repo,
549+
tree_sha: "HEAD",
550+
recursive: "true"
551+
}),
552+
async ({ data: allFiles }) => {
553+
const allPackageJson = allFiles.tree
554+
.map(({ path }) => path)
555+
.filter(path => path !== undefined)
556+
.filter(
557+
path =>
558+
!path.includes("/test/") &&
559+
!path.includes("/e2e-tests/") &&
560+
(path === "package.json" || path.endsWith("/package.json"))
561+
);
594562

595-
/**
596-
* Delete a repository from the cache
597-
*
598-
* @param owner the owner of the GitHub repository to remove
599-
* from the cache
600-
* @param repo the name of the GitHub repository to remove
601-
* from the cache
602-
* @param type the kind of cache to target
603-
*/
604-
async deleteEntry(owner: string, repo: string, type: KeyType) {
605-
const cacheKey = this.#getRepoKey(owner, repo, type);
606-
await this.#redis.del(cacheKey);
563+
const descriptions = new Map<string, string>();
564+
for (const path of allPackageJson) {
565+
const { data: packageJson } = await this.#octokit.rest.repos.getContent({
566+
owner,
567+
repo,
568+
path
569+
});
570+
571+
if (!("content" in packageJson)) continue; // filter out empty or multiple results
572+
const { content, encoding, type } = packageJson;
573+
if (type !== "file" || !content) continue; // filter out directories and empty files
574+
const packageFile =
575+
encoding === "base64" ? Buffer.from(content, "base64").toString() : content;
576+
577+
try {
578+
const { description } = JSON.parse(packageFile) as { description: string };
579+
if (description) descriptions.set(path, description);
580+
} catch {
581+
// ignore
582+
}
583+
}
584+
return Object.fromEntries(descriptions);
585+
},
586+
DESCRIPTIONS_TTL
587+
);
607588
}
608589
}
609590

0 commit comments

Comments
 (0)