UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

200 lines (164 loc) • 6.01 kB
import { DEV } from 'esm-env'; import { hash_request } from '../../utils/hash.js'; import { base64_decode } from '../utils.js'; import { fetch_cache_url } from '../shared.js'; import * as w from '../../messages/client-warnings.js'; let loading = 0; const native_fetch = window.fetch; export function lock_fetch() { loading += 1; } export function unlock_fetch() { loading -= 1; } if (DEV) { let can_inspect_stack_trace = false; // detect whether async stack traces work // eslint-disable-next-line @typescript-eslint/require-await const check_stack_trace = async () => { const stack = /** @type {string} */ (new Error().stack); can_inspect_stack_trace = stack.includes('check_stack_trace'); }; void check_stack_trace(); /** * @param {RequestInfo | URL} input * @param {RequestInit & Record<string, any> | undefined} init */ window.fetch = (input, init) => { // Check if fetch was called via load_node. the lock method only checks if it was called at the // same time, but not necessarily if it was called from `load`. // We use just the filename as the method name sometimes does not appear on the CI. const url = input instanceof Request ? input.url : input.toString(); const stack_array = /** @type {string} */ (new Error().stack).split('\n'); // We need to do a cutoff because Safari and Firefox maintain the stack // across events and for example traces a `fetch` call triggered from a button // back to the creation of the event listener and the element creation itself, // where at some point client.js will show up, leading to false positives. const cutoff = stack_array.findIndex((a) => a.includes('load@') || a.includes('at load')); const stack = stack_array.slice(0, cutoff + 2).join('\n'); const in_load_heuristic = can_inspect_stack_trace ? stack.includes('src/runtime/client/client.js') : loading; // This flag is set in initial_fetch and subsequent_fetch const used_kit_fetch = init?.__sveltekit_fetch__; if (in_load_heuristic && !used_kit_fetch) { w.window_fetch_in_load({ url }); } const method = input instanceof Request ? input.method : init?.method || 'GET'; if (method !== 'GET') { clear_cache(input); } return native_fetch(input, init); }; } else { window.fetch = (input, init) => { const method = input instanceof Request ? input.method : init?.method || 'GET'; if (method !== 'GET') { clear_cache(input); } return native_fetch(input, init); }; } const cache = new Map(); /** * Should be called on the initial run of load functions that hydrate the page. * Saves any requests with cache-control max-age to the cache. * @param {URL | string} resource * @param {RequestInit} [opts] */ export function initial_fetch(resource, opts) { const selector = build_selector(resource, opts); if (selector) { const script = document.querySelector(selector); if (script?.textContent) { script.remove(); // In case multiple script tags match the same selector let { body, ...init } = JSON.parse(script.textContent); const b64 = script.getAttribute('data-b64'); if (b64 !== null) { // Can't use native_fetch('data:...;base64,${body}') // csp can block the request body = base64_decode(body); } const ttl = script.getAttribute('data-ttl'); if (ttl) cache.set(selector, { body, init, ttl: 1000 * Number(ttl) }); return Promise.resolve(new Response(body, init)); } } return DEV ? dev_fetch(resource, opts) : window.fetch(resource, opts); } /** * Tries to get the response from the cache, if max-age allows it, else does a fetch. * @param {URL | string} resource * @param {string} resolved * @param {RequestInit} [opts] */ export function subsequent_fetch(resource, resolved, opts) { if (cache.size > 0) { const selector = build_selector(resource, opts); const cached = selector && cache.get(selector); if (cached) { // https://developer.mozilla.org/en-US/docs/Web/API/Request/cache#value if ( performance.now() < cached.ttl && ['default', 'force-cache', 'only-if-cached', undefined].includes(opts?.cache) ) { return new Response(cached.body, cached.init); } cache.delete(selector); } } return DEV ? dev_fetch(resolved, opts) : window.fetch(resolved, opts); } /** * @param {RequestInfo | URL} resource * @param {RequestInit & Record<string, any> | undefined} opts */ export function dev_fetch(resource, opts) { const patched_opts = { ...opts }; // This assigns the __sveltekit_fetch__ flag and makes it non-enumerable Object.defineProperty(patched_opts, '__sveltekit_fetch__', { value: true, writable: true, configurable: true }); return window.fetch(resource, patched_opts); } /** * Evict all cached responses for a URL, including responses keyed by request data * @param {RequestInfo | URL} input */ function clear_cache(input) { const selector = build_selector(requested_url(input)); for (const key of cache.keys()) { if (key.startsWith(selector)) cache.delete(key); } } /** * Non-GET requests must evict under the stored key, however the url is spelled * @param {RequestInfo | URL} input */ function requested_url(input) { return fetch_cache_url( new URL(input instanceof Request ? input.url : input, location.href), location ); } /** * Build the cache key for a given request * @param {URL | RequestInfo} resource * @param {RequestInit} [opts] * @returns {string | null} `null` for requests the server never serializes */ function build_selector(resource, opts) { const url = JSON.stringify(resource instanceof Request ? resource.url : resource); let selector = `script[data-sveltekit-fetched][data-url=${url}]`; if (opts?.headers || opts?.body) { const body = opts.body; if (body && typeof body !== 'string' && !ArrayBuffer.isView(body)) { // the server skips serializing these, so a matching script tag belongs to another request return null; } selector += `[data-hash="${hash_request(opts.headers, body)}"]`; } return selector; }