diff --git a/package.json b/package.json index 210dcf67..640919a3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "clsx": "^2.0.0", "next": "^14.0.0", + "node-cache": "^5.1.2", "react": "^18.2.0", "react-dom": "^18.2.0", "tidelift-me-up": "^0.4.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21b9907c..203db005 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: next: specifier: ^14.0.0 version: 14.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + node-cache: + specifier: ^5.1.2 + version: 5.1.2 react: specifier: ^18.2.0 version: 18.3.1 @@ -855,6 +858,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -1904,6 +1911,10 @@ packages: sass: optional: true + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3327,6 +3338,8 @@ snapshots: clone@1.0.4: optional: true + clone@2.1.2: {} + clsx@2.1.1: {} color-convert@1.9.3: @@ -4536,6 +4549,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 diff --git a/src/app/page.tsx b/src/app/page.tsx index d5a62517..e2e2544b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,34 +3,41 @@ import { MainArea } from "~/components/MainArea"; import { OptionsForm } from "~/components/OptionsForm"; import { ResultDisplay } from "~/components/ResultDisplay"; import { ScrollButton } from "~/components/ScrollButton"; -import { SearchParamsType, fetchData } from "~/utils/fetchData"; +import { fetchData } from "~/utils/fetchData"; +import { SearchParams, getOptions } from "~/utils/getOptions"; import { metadata as defaultMetadata } from "./layout"; import styles from "./page.module.css"; export interface HomeProps { - searchParams: SearchParamsType; + searchParams: SearchParams; } export async function generateMetadata({ searchParams }: HomeProps) { - const { options, result } = await fetchData(searchParams); - const username = options.username || ""; - const packageCount = Array.isArray(result) ? result.length : 0; + const options = getOptions(searchParams); + const username = options.username; if (!username) { return defaultMetadata; } + const result = await fetchData(options); + + const description = Array.isArray(result) + ? `${username} has ${result.length} npm package${ + result.length === 1 ? "" : "s" + } eligible for Tidelift funding. 💸` + : `Could not find packages for ${username}`; + return { - description: `${username} has ${packageCount} npm package${ - packageCount === 1 ? "" : "s" - } eligible for Tidelift funding. 💸`, + description, title: `${username} | Tidelift Me Up`, }; } export default async function Home({ searchParams }: HomeProps) { - const { options, result } = await fetchData(searchParams); + const options = getOptions(searchParams); + const result = await fetchData(options); return ( <> diff --git a/src/utils/fetchData.ts b/src/utils/fetchData.ts index d738f9af..b003a99d 100644 --- a/src/utils/fetchData.ts +++ b/src/utils/fetchData.ts @@ -1,44 +1,26 @@ -import { - EstimatedPackage, - PackageOwnership, - tideliftMeUp, -} from "tidelift-me-up"; +import NodeCache from "node-cache"; +import { EstimatedPackage, tideliftMeUp } from "tidelift-me-up"; -export type OptionsType = Record; -export type SearchParamsType = Record; +export type DataOptions = Record; +export type DataResults = Error | EstimatedPackage[] | undefined; -export async function fetchData(searchParams: SearchParamsType) { - const options = getOptions(searchParams); - const result = await getTideliftData(options); - return { options, result }; -} +const cache = new NodeCache(); -function getOptions(searchParams: SearchParamsType) { - return { - ownership: undefinedIfEmpty( - [ - searchParams.author === "on" && "author", - searchParams.maintainer === "on" && "maintainer", - searchParams.publisher === "on" && "publisher", - ].filter(Boolean) as PackageOwnership[], - ), - since: (searchParams.since || undefined) as string | undefined, - username: searchParams.username as string, - }; -} +export async function fetchData(options: DataOptions) { + const cacheKey = JSON.stringify(options); + + if (cache.has(cacheKey)) { + return cache.get(cacheKey); + } -async function getTideliftData(options: OptionsType) { - let result: Error | EstimatedPackage[] | undefined; + let result: DataResults; try { result = options.username ? await tideliftMeUp(options) : undefined; + cache.set(cacheKey, result); } catch (error) { result = error as Error; } return result; } - -function undefinedIfEmpty(items: T[]) { - return items.length === 0 ? undefined : items; -} diff --git a/src/utils/getOptions.ts b/src/utils/getOptions.ts new file mode 100644 index 00000000..42351589 --- /dev/null +++ b/src/utils/getOptions.ts @@ -0,0 +1,21 @@ +import { PackageOwnership } from "tidelift-me-up"; + +export type SearchParams = Record; + +export function getOptions(searchParams: SearchParams) { + return { + ownership: undefinedIfEmpty( + [ + searchParams.author === "on" && "author", + searchParams.maintainer === "on" && "maintainer", + searchParams.publisher === "on" && "publisher", + ].filter(Boolean) as PackageOwnership[], + ), + since: (searchParams.since || undefined) as string | undefined, + username: (searchParams.username || "") as string, + }; +} + +function undefinedIfEmpty(items: T[]) { + return items.length === 0 ? undefined : items; +}