| ... | @@ -21,15 +21,35 @@ type LicenseManifestState = | ... | @@ -21,15 +21,35 @@ type LicenseManifestState = |
| 21 | | { readonly status: "error"; readonly message: string } | 21 | | { readonly status: "error"; readonly message: string } |
| 22 | | { readonly status: "ready"; readonly manifest: ThirdPartyLicenseManifest }; | 22 | | { readonly status: "ready"; readonly manifest: ThirdPartyLicenseManifest }; |
| 23 | | 23 | |
| 24 | async function loadLicenseManifest(signal: AbortSignal): Promise<ThirdPartyLicenseManifest> { | 24 | let licenseManifestPromise: Promise<ThirdPartyLicenseManifest> | undefined; |
| 25 | const response = await fetch( | 25 | |
| 26 | `${import.meta.env.BASE_URL.replace(/\/$/, "")}/third-party-licenses.json`, | 26 | async function fetchLicenseManifest(): Promise<ThirdPartyLicenseManifest> { |
| 27 | { signal }, | 27 | const signal = AbortSignal.timeout(30_000); |
| 28 | ); | 28 | try { |
| 29 | if (!response.ok) { | 29 | const response = await fetch( |
| 30 | throw new Error(`The license manifest request failed with status ${String(response.status)}.`); | 30 | `${import.meta.env.BASE_URL.replace(/\/$/, "")}/third-party-licenses.json`, |
| | 31 | { signal }, |
| | 32 | ); |
| | 33 | if (!response.ok) { |
| | 34 | throw new Error( |
| | 35 | `The license manifest request failed with status ${String(response.status)}.`, |
| | 36 | ); |
| | 37 | } |
| | 38 | return decodeThirdPartyLicenseManifest((await response.json()) as unknown); |
| | 39 | } catch (error) { |
| | 40 | if (signal.aborted) { |
| | 41 | throw new Error("The license manifest took too long to load.", { cause: error }); |
| | 42 | } |
| | 43 | throw error; |
| 31 | } | 44 | } |
| 32 | return decodeThirdPartyLicenseManifest((await response.json()) as unknown); | 45 | } |
| | 46 | |
| | 47 | function loadLicenseManifest(): Promise<ThirdPartyLicenseManifest> { |
| | 48 | licenseManifestPromise ??= fetchLicenseManifest().catch((error: unknown) => { |
| | 49 | licenseManifestPromise = undefined; |
| | 50 | throw error; |
| | 51 | }); |
| | 52 | return licenseManifestPromise; |
| 33 | } | 53 | } |
| 34 | | 54 | |
| 35 | function LicenseNoticeRow({ | 55 | function LicenseNoticeRow({ |
| ... | @@ -198,19 +218,23 @@ export function OpenSourceLicensesPanel() { | ... | @@ -198,19 +218,23 @@ export function OpenSourceLicensesPanel() { |
| 198 | const [requestVersion, setRequestVersion] = useState(0); | 218 | const [requestVersion, setRequestVersion] = useState(0); |
| 199 | | 219 | |
| 200 | useEffect(() => { | 220 | useEffect(() => { |
| 201 | const controller = new AbortController(); | 221 | let cancelled = false; |
| 202 | setState({ status: "loading" }); | 222 | setState({ status: "loading" }); |
| 203 | void loadLicenseManifest(controller.signal).then( | 223 | void loadLicenseManifest().then( |
| 204 | (manifest) => setState({ status: "ready", manifest }), | 224 | (manifest) => { |
| | 225 | if (!cancelled) setState({ status: "ready", manifest }); |
| | 226 | }, |
| 205 | (error: unknown) => { | 227 | (error: unknown) => { |
| 206 | if (controller.signal.aborted) return; | 228 | if (cancelled) return; |
| 207 | setState({ | 229 | setState({ |
| 208 | status: "error", | 230 | status: "error", |
| 209 | message: error instanceof Error ? error.message : "The license manifest could not load.", | 231 | message: error instanceof Error ? error.message : "The license manifest could not load.", |
| 210 | }); | 232 | }); |
| 211 | }, | 233 | }, |
| 212 | ); | 234 | ); |
| 213 | return () => controller.abort(); | 235 | return () => { |
| | 236 | cancelled = true; |
| | 237 | }; |
| 214 | }, [requestVersion]); | 238 | }, [requestVersion]); |
| 215 | | 239 | |
| 216 | const entries = state.status === "ready" ? state.manifest.entries : []; | 240 | const entries = state.status === "ready" ? state.manifest.entries : []; |