|
3 | 3 | const response_bodies = $WPT_BODIES; |
4 | 4 | const script_response_bodies = $WPT_BODIES.filter(body => body.type === 'Script'); |
5 | 5 |
|
| 6 | +function fetchWithTimeout(url) { |
| 7 | + var controller = new AbortController(); |
| 8 | + setTimeout(() => {controller.abort()}, 5000); |
| 9 | + return fetch(url, {signal: controller.signal}); |
| 10 | +} |
| 11 | + |
6 | 12 | function getRawHtmlDocument() { |
7 | 13 | let rawHtml; |
8 | 14 | if (response_bodies.length > 0) { |
@@ -324,17 +330,53 @@ function getLcpResponseObject(lcpUrl) { |
324 | 330 | return responseObject; |
325 | 331 | } |
326 | 332 |
|
327 | | -function getSpeculationRules() { |
328 | | - return Array.from(document.querySelectorAll('script[type=speculationrules]')).map(script => { |
329 | | - try { |
330 | | - return JSON.parse(script.innerHTML); |
331 | | - } catch (error) { |
332 | | - return null; |
333 | | - } |
334 | | - }); |
| 333 | +function getParameterCaseInsensitive(object, key) { |
| 334 | + return object[Object.keys(object).find(k => k.toLowerCase() === key.toLowerCase())]; |
| 335 | +} |
| 336 | + |
| 337 | +async function getSpeculationRules() { |
| 338 | + // Get rules from the HTML |
| 339 | + const htmlRules = Array.from(document.querySelectorAll('script[type=speculationrules]')).map(script => { |
| 340 | + try { |
| 341 | + return JSON.parse(script.innerHTML); |
| 342 | + } catch (error) { |
| 343 | + return null; |
| 344 | + } |
| 345 | + }); |
| 346 | + |
| 347 | + // Get rules from the HTTP requests |
| 348 | + const documentRequests = [$WPT_REQUESTS.find( req => req.url === document.location.href)] || []; |
| 349 | + const httpRules = await Promise.all(documentRequests.map(async request => { |
| 350 | + try { |
| 351 | + const speculationRulesHeaders = getParameterCaseInsensitive(request.response_headers, 'Speculation-Rules'); |
| 352 | + if (speculationRulesHeaders) { |
| 353 | + return await Promise.all(speculationRulesHeaders.split(',').map(async (speculationRuleLocation) => { |
| 354 | + try { |
| 355 | + let url = decodeURI(speculationRuleLocation).slice(1, -1); |
| 356 | + if (url.startsWith('/')) { |
| 357 | + url = document.location.origin + url; |
| 358 | + } else if (!url.startsWith('http')) { |
| 359 | + url = document.location.href + url; |
| 360 | + } |
| 361 | + const response = await fetchWithTimeout(url); |
| 362 | + const body = await response.text(); |
| 363 | + return {url: speculationRuleLocation, rule: JSON.parse(body)}; |
| 364 | + } catch(error) { |
| 365 | + return {error}; |
| 366 | + } |
| 367 | + })); |
| 368 | + } else { |
| 369 | + return []; |
| 370 | + } |
| 371 | + } catch(error) { |
| 372 | + return {error}; |
| 373 | + }; |
| 374 | + })); |
| 375 | + |
| 376 | + return {htmlRules: htmlRules, httpHeaderRules: httpRules}; |
335 | 377 | } |
336 | 378 |
|
337 | | -return Promise.all([getLcpElement()]).then(([lcp_elem_stats]) => { |
| 379 | +return Promise.all([getLcpElement(), getSpeculationRules()]).then(([lcp_elem_stats, speculation_rules]) => { |
338 | 380 | const lcpUrl = lcp_elem_stats.url; |
339 | 381 | const rawDoc = getRawHtmlDocument(); |
340 | 382 | // Start out with true, only if LCP element is an external resource will we eval & potentially set to false. |
@@ -366,7 +408,7 @@ return Promise.all([getLcpElement()]).then(([lcp_elem_stats]) => { |
366 | 408 | lcp_preload: lcpPreload, |
367 | 409 | web_vitals_js: getWebVitalsJS(), |
368 | 410 | gaming_metrics: gamingMetrics, |
369 | | - speculation_rules: getSpeculationRules(), |
| 411 | + speculation_rules: speculation_rules, |
370 | 412 | }; |
371 | 413 | }).catch(error => { |
372 | 414 | return {error}; |
|
0 commit comments