Skip to content

Commit 08b9067

Browse files
feat: implement backend
1 parent 7d29fc8 commit 08b9067

File tree

2 files changed

+92
-20
lines changed

2 files changed

+92
-20
lines changed

src/lib/server/github-cache.ts

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type GitHubRelease = Awaited<
1818
>["data"][number];
1919

2020
type KeyType = "releases" | "descriptions" | "issue" | "pr";
21+
type CachedValue<T> = { value: T; cache: boolean };
2122

2223
export type ItemDetails = {
2324
comments: Awaited<ReturnType<Issues["listComments"]>>["data"];
@@ -70,6 +71,10 @@ const FULL_DETAILS_TTL = 60 * 60 * 2; // 2 hours
7071
* The TTL of the cached descriptions, in seconds.
7172
*/
7273
const DESCRIPTIONS_TTL = 60 * 60 * 24 * 10; // 10 days
74+
/**
75+
* The TTL for non-deprecated packages, in seconds
76+
*/
77+
const DEPRECATIONS_TTL = 60 * 60 * 24 * 2; // 2 days
7378

7479
/**
7580
* A fetch layer to reach the GitHub API
@@ -115,6 +120,21 @@ export class GitHubCache {
115120
return `repo:${owner}/${repo}:${type}${strArgs}`;
116121
}
117122

123+
/**
124+
* Generates a Redis key from the passed info.
125+
*
126+
* @param packageName the package name
127+
* @param args the optional additional values to append
128+
* at the end of the key; every element will be interpolated
129+
* in a string
130+
* @returns the pure computed key
131+
* @private
132+
*/
133+
#getPackageKey(packageName: string, ...args: unknown[]) {
134+
const strArgs = args.map(a => `:${a}`);
135+
return `package:${packageName}${strArgs}`;
136+
}
137+
118138
/**
119139
* An abstraction over general processing that:
120140
* 1. tries getting stuff from Redis cache
@@ -124,6 +144,10 @@ export class GitHubCache {
124144
* @private
125145
*/
126146
#processCached<RType extends Parameters<InstanceType<typeof Redis>["json"]["set"]>[2]>() {
147+
function isCachedValue<T>(item: T | CachedValue<T>): item is CachedValue<T> {
148+
return typeof item === "object" && item !== null && "value" in item && "cache" in item;
149+
}
150+
127151
/**
128152
* Inner currying function to circumvent unsupported partial inference
129153
*
@@ -137,8 +161,10 @@ export class GitHubCache {
137161
return async <PromiseType>(
138162
cacheKey: string,
139163
promise: () => Promise<PromiseType>,
140-
transformer: (from: Awaited<PromiseType>) => RType | Promise<RType>,
141-
ttl: number | undefined = undefined
164+
transformer: (
165+
from: Awaited<PromiseType>
166+
) => RType | CachedValue<RType> | Promise<RType> | Promise<CachedValue<RType>>,
167+
ttl: number | ((value: RType) => number | undefined) | undefined = undefined
142168
): Promise<RType> => {
143169
const cachedValue = await this.#redis.json.get<RType>(cacheKey);
144170
if (cachedValue) {
@@ -148,11 +174,25 @@ export class GitHubCache {
148174

149175
console.log(`Cache miss for ${cacheKey}`);
150176

151-
const newValue = await transformer(await promise());
177+
let newValue = await transformer(await promise());
178+
let wantsCache = true;
179+
if (isCachedValue(newValue)) {
180+
wantsCache = newValue.cache;
181+
newValue = newValue.value;
182+
}
152183

153-
await this.#redis.json.set(cacheKey, "$", newValue);
154-
if (ttl !== undefined) {
155-
await this.#redis.expire(cacheKey, ttl);
184+
if (wantsCache) {
185+
await this.#redis.json.set(cacheKey, "$", newValue);
186+
if (ttl !== undefined) {
187+
if (typeof ttl === "function") {
188+
const ttlResult = ttl(newValue);
189+
if (ttlResult !== undefined) {
190+
await this.#redis.expire(cacheKey, ttlResult);
191+
}
192+
} else {
193+
await this.#redis.expire(cacheKey, ttl);
194+
}
195+
}
156196
}
157197

158198
return newValue;
@@ -537,7 +577,6 @@ export class GitHubCache {
537577
* @param repo the GitHub repository name to fetch the
538578
* descriptions in
539579
* @returns a map of paths to descriptions.
540-
* @private
541580
*/
542581
async getDescriptions(owner: string, repo: string) {
543582
return await this.#processCached<{ [key: string]: string }>()(
@@ -586,6 +625,35 @@ export class GitHubCache {
586625
DESCRIPTIONS_TTL
587626
);
588627
}
628+
629+
/**
630+
* Get the deprecation state of a package from its name.
631+
*
632+
* @param packageName the name of the package to search
633+
* @returns the deprecation status message if any, `false` otherwise
634+
*/
635+
async getPackageDeprecation(packageName: string) {
636+
return await this.#processCached<string | false>()(
637+
this.#getPackageKey(packageName, "deprecation"),
638+
async () => {
639+
try {
640+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
641+
if (res.status !== 200) return {};
642+
return (await res.json()) as { deprecated?: boolean | string };
643+
} catch (error) {
644+
console.error(`Error fetching npmjs.org for package ${packageName}:`, error);
645+
return {};
646+
}
647+
},
648+
({ deprecated }) => {
649+
if (deprecated === undefined) return false;
650+
if (typeof deprecated === "boolean")
651+
return { value: "This package is deprecated", cache: false };
652+
return { value: deprecated || "This package is deprecated", cache: false };
653+
},
654+
item => (item === false ? DEPRECATIONS_TTL : undefined)
655+
);
656+
}
589657
}
590658

591659
export const gitHubCache = new GitHubCache(KV_REST_API_URL, KV_REST_API_TOKEN, GITHUB_TOKEN);

src/lib/server/package-discoverer.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GitHubCache, gitHubCache } from "./github-cache";
55
type Package = {
66
name: string;
77
description: string;
8+
deprecated?: string;
89
};
910

1011
export type DiscoveredPackage = Prettify<
@@ -54,19 +55,22 @@ export class PackageDiscoverer {
5455
);
5556
return {
5657
...repo,
57-
packages: packages.map(pkg => {
58-
const ghName = this.#gitHubDirectoryFromName(pkg);
59-
return {
60-
name: pkg,
61-
description:
62-
descriptions[`packages/${ghName}/package.json`] ??
63-
descriptions[
64-
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
65-
] ??
66-
descriptions["package.json"] ??
67-
""
68-
};
69-
})
58+
packages: await Promise.all(
59+
packages.map(async (pkg): Promise<Package> => {
60+
const ghName = this.#gitHubDirectoryFromName(pkg);
61+
return {
62+
name: pkg,
63+
description:
64+
descriptions[`packages/${ghName}/package.json`] ??
65+
descriptions[
66+
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
67+
] ??
68+
descriptions["package.json"] ??
69+
"",
70+
deprecated: (await this.#cache.getPackageDeprecation(pkg)) || undefined
71+
};
72+
})
73+
)
7074
};
7175
})
7276
);

0 commit comments

Comments
 (0)