diff --git a/src/app/[...parts]/_page/NpmDiff/DiffTooLarge.tsx b/src/app/[...parts]/_page/NpmDiff/DiffTooLarge.tsx new file mode 100644 index 00000000..98b8f22f --- /dev/null +++ b/src/app/[...parts]/_page/NpmDiff/DiffTooLarge.tsx @@ -0,0 +1,37 @@ +import { type ComponentProps, forwardRef } from "react"; +import Code from "^/components/ui/Code"; +import Heading from "^/components/ui/Heading"; +import Pkg from "^/components/ui/Pkg"; +import { cx } from "^/lib/cva"; +import type SimplePackageSpec from "^/lib/SimplePackageSpec"; + +export interface DiffTooLargeProps extends ComponentProps<"section"> { + a: SimplePackageSpec; + b: SimplePackageSpec; + specs: [string, string]; +} + +const DiffTooLarge = forwardRef( + ({ a, b, specs, className, ...props }, ref) => ( +
+

📦🐘

+ + This diff is too large! + +

+ Comparing and took too long. You + can run it locally instead: +

+ + npm diff --diff={specs[0]} --diff={specs[1]} + +
+ ), +); +DiffTooLarge.displayName = "DiffTooLarge"; + +export default DiffTooLarge; diff --git a/src/app/[...parts]/_page/NpmDiff/NpmDiff.tsx b/src/app/[...parts]/_page/NpmDiff/NpmDiff.tsx index 36b8587f..e87d7c36 100644 --- a/src/app/[...parts]/_page/NpmDiff/NpmDiff.tsx +++ b/src/app/[...parts]/_page/NpmDiff/NpmDiff.tsx @@ -8,6 +8,7 @@ import npmDiff from "^/lib/npmDiff"; import type SimplePackageSpec from "^/lib/SimplePackageSpec"; import countChanges from "^/lib/utils/countChanges"; import DiffFiles from "./DiffFiles"; +import DiffTooLarge from "./DiffTooLarge"; import NoDiff from "./NoDiff"; import ViewTypeSwitch from "./ViewTypeSwitch"; @@ -26,6 +27,9 @@ const NpmDiff = async ({ a, b, specs, options }: NpmDiffProps) => { const result = await npmDiff(specs, options); if (!result.ok) { + if (result.kind === "timeout") { + return ; + } throw new Error(result.message); } diff --git a/src/app/api/[...parts]/route.ts b/src/app/api/[...parts]/route.ts index 8f4b9d0b..1b363133 100644 --- a/src/app/api/[...parts]/route.ts +++ b/src/app/api/[...parts]/route.ts @@ -57,7 +57,12 @@ export async function GET(req: NextRequest, { params }: DiffApiContext) { headers: defaultPageCachingHeaders, }); } else { - const status = result.kind === "not-found" ? 404 : 500; + const status = + result.kind === "not-found" + ? 404 + : result.kind === "timeout" + ? 504 + : 500; return NextResponse.json(result.message, { status }); } } else { diff --git a/src/lib/npmDiff/NpmDiffError.ts b/src/lib/npmDiff/NpmDiffError.ts index ac680335..a32c6fee 100644 --- a/src/lib/npmDiff/NpmDiffError.ts +++ b/src/lib/npmDiff/NpmDiffError.ts @@ -1,3 +1,4 @@ export type NpmDiffError = | { kind: "not-found"; message: string } + | { kind: "timeout"; message: string } | { kind: "unknown"; message: string }; diff --git a/src/lib/npmDiff/libnpmdiff.worker.ts b/src/lib/npmDiff/libnpmdiff.worker.ts new file mode 100644 index 00000000..463ca7f9 --- /dev/null +++ b/src/lib/npmDiff/libnpmdiff.worker.ts @@ -0,0 +1,41 @@ +import libnpmdiff, { type Options } from "libnpmdiff"; +import { parentPort, workerData } from "node:worker_threads"; + +export interface LibnpmdiffWorkerData { + specs: [string, string]; + options: Options; +} + +export type LibnpmdiffWorkerMessage = + | { ok: true; diff: string } + | { + ok: false; + error: { + code?: unknown; + type?: unknown; + wanted?: unknown; + pkgid?: unknown; + message?: unknown; + }; + }; + +const { specs, options } = workerData as LibnpmdiffWorkerData; + +libnpmdiff(specs, options).then( + (diff) => { + parentPort?.postMessage({ + ok: true, + diff, + } satisfies LibnpmdiffWorkerMessage); + }, + (e: unknown) => { + const { code, type, wanted, pkgid, message } = (e ?? {}) as Record< + string, + unknown + >; + parentPort?.postMessage({ + ok: false, + error: { code, type, wanted, pkgid, message }, + } satisfies LibnpmdiffWorkerMessage); + }, +); diff --git a/src/lib/npmDiff/npmDiff.ts b/src/lib/npmDiff/npmDiff.ts index cf665c4f..2d88525b 100644 --- a/src/lib/npmDiff/npmDiff.ts +++ b/src/lib/npmDiff/npmDiff.ts @@ -1,7 +1,10 @@ -import libnpmdiff, { type Options } from "libnpmdiff"; +import type { Options } from "libnpmdiff"; import { cacheLife } from "next/cache"; import { hasErrorCode } from "^/lib/utils/hasErrorCode"; import type { NpmDiffError } from "./NpmDiffError"; +import { LibnpmdiffTimeoutError, runLibnpmdiff } from "./runLibnpmdiff"; + +export const NPM_DIFF_TIMEOUT_MS = 40_000; export type NpmDiffResult = { ok: true; diff: string } | ({ ok: false } & NpmDiffError); @@ -29,7 +32,7 @@ async function npmDiff( let startTime = 0; try { startTime = Date.now(); - const result = await libnpmdiff(specs, options); + const result = await runLibnpmdiff(specs, options, NPM_DIFF_TIMEOUT_MS); cacheLife("max"); @@ -48,6 +51,16 @@ async function npmDiff( duration: Date.now() - startTime, }); + if (e instanceof LibnpmdiffTimeoutError) { + cacheLife("days"); + + return { + ok: false, + kind: "timeout", + message: `The diff took too long to compute. Run "npm diff --diff=${specs[0]} --diff=${specs[1]}" locally instead.`, + }; + } + const isEtarget = (e: unknown): e is ErrorETARGET => hasErrorCode(e, "ETARGET"); const isE404 = (e: unknown): e is Error404 => hasErrorCode(e, "E404"); diff --git a/src/lib/npmDiff/runLibnpmdiff.ts b/src/lib/npmDiff/runLibnpmdiff.ts new file mode 100644 index 00000000..18dac517 --- /dev/null +++ b/src/lib/npmDiff/runLibnpmdiff.ts @@ -0,0 +1,53 @@ +import type { Options } from "libnpmdiff"; +import { Worker } from "node:worker_threads"; +import type { + LibnpmdiffWorkerData, + LibnpmdiffWorkerMessage, +} from "./libnpmdiff.worker"; + +export class LibnpmdiffTimeoutError extends Error { + constructor(timeoutMs: number) { + super(`libnpmdiff did not finish within ${timeoutMs}ms`); + this.name = "LibnpmdiffTimeoutError"; + } +} + +export function runLibnpmdiff( + specs: [string, string], + options: Options, + timeoutMs: number, +): Promise { + return new Promise((resolve, reject) => { + const worker = new Worker( + new URL("./libnpmdiff.worker.ts", import.meta.url), + { + workerData: { specs, options } satisfies LibnpmdiffWorkerData, + }, + ); + + const timer = setTimeout(() => { + reject(new LibnpmdiffTimeoutError(timeoutMs)); + void worker.terminate(); + }, timeoutMs); + + worker.once("message", (message: LibnpmdiffWorkerMessage) => { + clearTimeout(timer); + if (message.ok) { + resolve(message.diff); + } else { + reject(Object.assign(new Error(), message.error)); + } + void worker.terminate(); + }); + + worker.once("error", (e) => { + clearTimeout(timer); + reject(e); + }); + + worker.once("exit", (code) => { + clearTimeout(timer); + reject(new Error(`libnpmdiff worker exited with code ${code}`)); + }); + }); +}