UNPKG

@sveltejs/kit

Version:

SvelteKit is the fastest way to build Svelte apps

3,813 lines • 113 kB
/** @import { LoadEvent, NavigationEvent } from '@sveltejs/kit' */
/** @import { ActionResult } from '$app/forms' */
/** @import { AfterNavigate, BeforeNavigate, GotoOptions, Navigation, NavigationTarget, NavigationType, OnNavigate } from '$app/navigation' */
/** @import { RemoteFunctionDataNode, ServerNodesResponse, ServerRedirectNode } from 'types' */
/** @import { NavigationFinished, NavigationIntent } from './types.js' */
/** @import { CacheEntry } from './remote-functions/cache.svelte.js' */
/** @import { Query } from './remote-functions/query/instance.svelte.js' */
/** @import { LiveQuery } from './remote-functions/query-live/instance.svelte.js' */
import { DEV } from 'esm-env';
import { settled, tick, fork, onMount, hydrate, mount } from 'svelte';
import { HttpError, Redirect, SvelteKitError, HandledHttpError } from '@sveltejs/kit/internal';
import { decode_pathname, strip_hash, make_trackable, normalize_path } from '../../utils/url.js';
import { dev_fetch, initial_fetch, lock_fetch, subsequent_fetch, unlock_fetch } from './fetcher.js';
import { parse_routes, parse_server_route } from './parse.js';
import * as storage from './session-storage.js';
import { blur_active_element, reset_focus } from './focus.js';
import { disable_scroll_handling, restore_scroll } from './scroll.js';
import {
	find_anchor,
	resolve_url,
	get_link_info,
	get_router_options,
	is_external_url,
	origin,
	scroll_state,
	load_css
} from './utils.js';
import { base, set_match_implementation } from '$app/paths/internal/client';
import * as devalue from 'devalue';
import {
	HISTORY_INFO_KEY,
	HISTORY_METADATA_KEY,
	PRELOAD_PRIORITIES,
	SNAPSHOT_KEY
} from './constants.js';
import {
	capture_navigation_snapshot,
	current_registrations,
	delete_navigation_snapshot,
	init_snapshots,
	persist_navigation_snapshots,
	restore_navigation_snapshot
} from './snapshots.js';
import { validate_page_exports } from '../../utils/exports.js';
import { add_deprecated_handle_error_properties } from '../../utils/error.js';
import { noop } from '../../utils/functions.js';
import {
	INVALIDATED_PARAM,
	TRAILING_SLASH_PARAM,
	create_remote_key,
	validate_depends,
	validate_load_response,
	fetch_cache_url
} from '../shared.js';

import { page, updated, notify_version, update_page, set_navigation } from '#app/state/client';
import { payload } from './payload.js';
import {
	add_data_suffix,
	add_resolution_suffix,
	route_id_resolution_pathname
} from '../pathname.js';
import { noop_span } from '../../telemetry.js';
import { read_ndjson } from './ndjson.js';
import Root from '../components/root.svelte';
import { Props, RenderNode } from '../props.svelte.js';
import { init_transport, parse, stringify } from '#app/internal/transport';
import { build_error_chain, nearest_error_pages } from '../error-chain.js';
import * as e from '../../messages/client-errors.js';
import * as w from '../../messages/client-warnings.js';
import * as shared_errors from '../../messages/shared-errors.js';
import { capture_error } from '../../messages/internal/shared.js';

/**
 * @typedef {{
 *   historyIndex: number;
 *   navigationIndex: number;
 *   pageUrl?: string;
 *   state: string;
 *   persistState: boolean;
 *   resetIndex: number;
 * }} HistoryMetadata
 */

export { load_css };
const ICON_REL_ATTRIBUTES = new Set(['icon', 'shortcut icon', 'apple-touch-icon']);

let errored = false;

/**
 * `reset` functions for `<svelte:boundary>`s in the generated root that have
 * failed. A failed boundary stays failed until `reset()` is called — prop
 * updates alone don't re-render its content — so without resetting, a client
 * navigation away from a render error would leave the stale `+error.svelte`
 * mounted. The boundary's `onerror` populates this array; `navigate` drains it
 * after applying the new props. See sveltejs/kit#15694.
 * @type {Set<() => void>}
 */
const resetters = new Set();

// We track information associated with each history entry in sessionStorage,
// rather than on history.state itself, because when navigation is driven by
// popstate it's too late to access the options or update the focus position associated with the
// state we're navigating from
/**
 * @type {Record<number, { scroll?: { x: number; y: number }; resetIndex?: number }>}
 */
const history_info = storage.get(HISTORY_INFO_KEY) ?? {};

/**
 * navigation index -> any
 * @type {Record<string, any[]>}
 * @deprecated TODO 4.0 get rid of this
 */
const snapshots = storage.get(SNAPSHOT_KEY) ?? {};

init_snapshots(() => (started && !is_navigating && !updating ? current_history_index : null));

/** @type {Props} */
let props;

if (DEV) {
	let warned = false;

	const current_module_url = import.meta.url.split('?')[0]; // remove query params that vite adds to the URL when it is loaded from node_modules

	const warn = () => {
		if (warned) return;

		// Rather than saving a pointer to the original history methods, which would prevent monkeypatching by other libs,
		// inspect the stack trace to see if we're being called from within SvelteKit.
		let stack = new Error().stack?.split('\n');
		if (!stack) return;
		if (!stack[0].includes('https:') && !stack[0].includes('http:')) stack = stack.slice(1); // Chrome includes the error message in the stack

		// skip over `warn` and the place where `warn` was called
		const frame = stack[2];

		// Ignore calls that happen inside dependencies, including SvelteKit.
		// The second condition is only relevant when developing SvelteKit and running it, as there's no node_modules in the stack then (but we still do it to not get repeatedly confused)
		// `frame` can be falsy if we came from an anonymous function
		if (frame?.includes('node_modules') || frame?.includes(current_module_url)) return;

		warned = true;

		w.history_api_conflict();
	};

	const push_state = history.pushState;
	history.pushState = (...args) => {
		warn();
		return push_state.apply(history, args);
	};

	const replace_state = history.replaceState;
	history.replaceState = (...args) => {
		warn();
		return replace_state.apply(history, args);
	};
}

/** @param {number} index */
function capture_scroll(index) {
	history_info[index].scroll = scroll_state();
}

/**
 * @param {number} index
 * @param {Pick<HistoryMetadata, 'resetIndex'>} options
 */
function set_history_options(index, options) {
	history_info[index] = {
		...history_info[index],
		resetIndex: options.resetIndex
	};
}

/**
 * @param {number} current_history_index
 * @param {number} current_navigation_index
 */
function clear_onward_history(current_history_index, current_navigation_index) {
	// if we navigated back, then pushed a new state, we can
	// release memory by pruning the scroll/snapshot lookup
	// the new entry reuses the first abandoned index, so its slot must start clean
	delete_navigation_snapshot(current_history_index);
	let i = current_history_index + 1;
	while (history_info[i]) {
		delete history_info[i];
		delete_navigation_snapshot(i);
		i += 1;
	}

	i = current_navigation_index + 1;
	while (snapshots[i]) {
		delete snapshots[i];
		i += 1;
	}
}

/**
 * Loads `href` the old-fashioned way, with a full page reload.
 * Returns a `Promise` that never resolves (to prevent any
 * subsequent work, e.g. history manipulation, from happening)
 * @param {URL} url
 * @param {boolean} [replace] if `true`, will replace the current `history` entry rather than creating a new one with `pushState`
 * @returns {Promise<any>} a promise that never resolves
 */
function native_navigation(url, replace = false) {
	if (replace) {
		location.replace(url.href);
	} else {
		location.href = url.href;
	}
	return new Promise(noop);
}

/**
 * Checks whether a service worker is registered, and if it is,
 * tries to update it.
 */
async function update_service_worker() {
	if ('serviceWorker' in navigator) {
		const registration = await navigator.serviceWorker.getRegistration(base || '/');
		if (registration) {
			await registration.update();
		}
	}
}

/** @type {import('types').CSRRoute[]} All routes of the app. Only available when router.resolution=client */
let routes;
/** @type {import('types').CSRPageNodeLoader} */
let default_layout_loader;
/** @type {import('types').CSRPageNodeLoader} */
let default_error_loader;
/** @type {HTMLElement} */
let container;
/** @type {HTMLElement} */
let target;

/** @type {import('./types.js').SvelteKitApp} */
export let app;

/**
 * Data that was serialized during SSR for queries/forms/commands, stored as
 * `{ v }` (value) or `{ e }` (error) nodes so that failed states survive hydration.
 * Entries are deleted as they are consumed (when the corresponding resource is created).
 * @type {Record<string, RemoteFunctionDataNode>}
 */
export const query_responses = {};

/**
 * Data that was serialized during SSR for prerender functions, stored as
 * `{ v }` (value) or `{ e }` (error) nodes.
 * This persists across client-side navigations.
 * @type {Record<string, RemoteFunctionDataNode>}
 */
export const prerender_responses = {};

/** @type {Array<((url: URL) => boolean)>} */
const invalidated = [];

/** @type {{id: string, token: {}, promise: Promise<import('./types.js').NavigationResult>, fork: Promise<import('svelte').Fork | null> | null} | null} */
let load_cache = null;

function discard_load_cache() {
	void load_cache?.fork?.then((f) => f?.discard());
	load_cache = null;
	current_a = { element: undefined, href: undefined };
}

/**
 * @type {Map<string, Promise<URL>>}
 * Cache for client-side rerouting, since it could contain async calls which we want to
 * avoid running multiple times which would slow down navigations (e.g. else preloading
 * wouldn't help because on navigation it would be called again). Since `reroute` should be
 * a pure function (i.e. always return the same) value it's safe to cache across navigations.
 * The server reroute calls don't need to be cached because they are called using `import(...)`
 * which is cached per the JS spec.
 */
const reroute_cache = new Map();

/**
 * Sentinel for a route that exists but has no code to preload, i.e. a `+server.js` with no
 * `+page`. Cached like a parsed route so that repeated `preloadCode(id)` calls for the same
 * id don't re-request it.
 */
const ENDPOINT_ONLY = Symbol('endpoint only');

/**
 * Cache of route ID -> parsed route for server-side route resolution.
 * Populated whenever a server route resolution occurs (`match`, link preloading,
 * hydration), so that `preloadCode(id)` doesn't need an extra server round trip.
 * Lives until full page reload.
 * @type {Map<string, import('types').CSRRoute | typeof ENDPOINT_ONLY>}
 */
const route_id_cache = new Map();

/**
 * Parse a server-provided route and record it in `route_id_cache`, so that a subsequent
 * `preloadCode(id)` for the same route doesn't need another server round trip. Always use
 * this rather than calling `parse_server_route` directly.
 * @param {import('types').CSRRouteServer} server_route
 * @returns {import('types').CSRRoute}
 */
function parse_and_cache_server_route(server_route) {
	const route = parse_server_route(server_route, app.nodes);
	route_id_cache.set(route.id, route);
	return route;
}

/**
 * Note on before_navigate_callbacks, on_navigate_callbacks and after_navigate_callbacks:
 * do not re-assign as some closures keep references to these Sets
 */
/** @type {Set<(navigation: BeforeNavigate) => void>} */
const before_navigate_callbacks = new Set();

/** @type {Set<(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */
const on_navigate_callbacks = new Set();

/** @type {Set<(navigation: AfterNavigate) => void>} */
const after_navigate_callbacks = new Set();

/** @type {import('./types.js').NavigationState & { nav: NavigationEvent }} */
let current = {
	branch: [],
	error: null,
	// @ts-ignore - we need the initial value to be null
	url: null,
	// @ts-ignore - we need the initial value to be null
	nav: null
};

/** this being true means we SSR'd */
let hydrated = false;
let started = false;
let updating = false;
let is_navigating = false;
/** @type {HistoryMetadata | null} */
let hash_navigating = null;
/** True as soon as there happened one client-side navigation (excluding the SvelteKit-initialized initial one when in SPA mode) */
let has_navigated = false;

let force_invalidation = false;

/** @type {number} keeping track of the history index in order to prevent popstate navigation events if needed */
let current_history_index;

/** @type {number} */
let current_navigation_index;

/** @type {number} */
let current_reset_index;

/**
 * @param {any} [state]
 * @returns {HistoryMetadata | undefined}
 */
function get_history_metadata(state = history.state) {
	return state?.[HISTORY_METADATA_KEY];
}

/** @type {{}} Token for the latest navigation. Updated on new navigations */
let navigation_token;

/**
 * @type {{}}
 * The latest invalidate(All) token. Superseeded by both later invalidate(All)s and navigations.
 * This is separate to navigation_token because an invalidate(All) might be triggered while a navigation
 * is in progress, and we want to be able to finish this navigation (unless the invalidation finishes before
 * it and redirects, in which case we will do the redirect triggered by the invalidation).
 */
let invalidation_token;

/**
 * A set of tokens which are associated to current preloads.
 * If a preload becomes a real navigation, it's removed from the set.
 * If a preload token is in the set and the preload errors, the error
 * handling logic (for example reloading) is skipped.
 */
/** @type {Set<{}>} */
const preload_tokens = new Set();

/** @type {Promise<void> | null} */
let pending_invalidate;

/**
 * @type {Map<string, Map<string, CacheEntry<Query<any>>>>}
 * A map of query id -> payload -> query internals for all active queries.
 */
export const query_map = new Map();

/**
 * @type {Map<string, Map<string, CacheEntry<LiveQuery<any>>>>}
 * A map of id -> payload -> live query internals for all active queries.
 */
export const live_query_map = new Map();

set_match_implementation(async (url) => {
	if (typeof url === 'string') {
		url = new URL(url, location.href);
	}

	const intent = await get_navigation_intent(url, false);

	if (intent) {
		return {
			id: /** @type {import('$app/types').RouteId} */ (intent.route.id),
			params: intent.params
		};
	}

	return null;
});

let embedded_start = Promise.resolve();

/**
 * TODO this indirection is a grotesque workaround for the fact that multiple apps
 * can operate on the same shared mutable state. This is fundamentally unsound,
 * and the `start`/`_start` distinction does not fix it, but it does get the
 * tests passing. We need to rethink this whole thing but not right now
 * @param {import('./types.js').SvelteKitApp} _app
 * @param {HTMLElement} _target
 * @param {Parameters<typeof _hydrate>[1]} [data]
 */
export function start(_app, _target, data) {
	if (__SVELTEKIT_EMBEDDED__) {
		const start_promise = embedded_start.then(() => _start(_app, _target, data));
		embedded_start = start_promise.catch(noop);
		return start_promise;
	}

	return _start(_app, _target, data);
}

/**
 * @template T
 * @param {Map<string, Map<string, T>>} map
 * @returns {Generator<[string, T]>} every entry of the cache map, keyed by remote key
 */
function* cache_entries(map) {
	for (const [id, entries] of map) {
		for (const [payload, entry] of entries) {
			yield [create_remote_key(id, payload), entry];
		}
	}
}

/**
 * @param {import('./types.js').SvelteKitApp} _app
 * @param {HTMLElement} _target
 * @param {Parameters<typeof _hydrate>[1]} [data]
 */
async function _start(_app, _target, data) {
	if (DEV && _target === document.body) {
		w.app_body_unwrapped({ tag: '%sveltekit.body%' });
	}

	if (payload.data) {
		const { q = {}, p = {}, l = {}, f = {} } = payload.data;

		// store the whole nodes — error records seed the corresponding
		// resources in a failed state when they are created during hydration
		for (const k in q) query_responses[k] = q[k];
		for (const k in l) query_responses[k] = l[k];
		for (const k in f) query_responses[k] = f[k];
		for (const k in p) prerender_responses[k] = p[k];
	}

	// detect basic auth credentials in the current URL
	// https://github.com/sveltejs/kit/pull/11179
	// if so, refresh the page without credentials
	if (document.URL !== location.href) {
		// eslint-disable-next-line no-self-assign
		location.href = location.href;
	}

	app = _app;

	init_transport(app.hooks.transport ?? {});

	await _app.hooks.init?.();

	routes = __SVELTEKIT_CLIENT_ROUTING__ ? parse_routes(_app) : [];
	container = __SVELTEKIT_EMBEDDED__ ? _target : document.documentElement;
	target = _target;

	// we import the root layout/error nodes eagerly, so that
	// connectivity errors after initialisation don't nuke the app
	default_layout_loader = _app.nodes[0];
	default_error_loader = _app.nodes[1];

	const [root_layout] = await Promise.all([default_layout_loader(), default_error_loader()]);

	// the root boundary stays unarmed: the root +error.svelte renders at the node below it
	const tree = new RenderNode(root_layout.component, undefined);

	props = new Props({
		page,
		tree,
		form: undefined,
		error: undefined,
		onerror: (_, reset) => resetters.add(reset)
	});

	const history_metadata = get_history_metadata();
	current_history_index = history_metadata?.historyIndex ?? 0;
	current_navigation_index = history_metadata?.navigationIndex ?? 0;
	current_reset_index = history_metadata?.resetIndex ?? 0;

	if (!current_history_index) {
		// we use Date.now() as an offset so that cross-document navigations
		// within the app don't result in data loss
		current_history_index = current_navigation_index = current_reset_index = Date.now();

		// create initial history entry, so we can return here
		history.replaceState(
			{
				...history.state,
				[HISTORY_METADATA_KEY]: {
					historyIndex: current_history_index,
					navigationIndex: current_navigation_index,
					state: stringify({}),
					persistState: false,
					resetIndex: current_history_index
				}
			},
			''
		);
	}

	set_history_options(
		current_history_index,
		/** @type {HistoryMetadata} */ (get_history_metadata())
	);

	// if we reload the page, or Cmd-Shift-T back to it,
	// recover scroll position
	const scroll = history_info[current_history_index]?.scroll;
	function restore_reload_scroll() {
		if (scroll) {
			history.scrollRestoration = 'manual';
			scrollTo(scroll.x, scroll.y);
		}
	}

	if (data) {
		restore_reload_scroll();

		await _hydrate(target, data);
	} else {
		await navigate({
			type: 'enter',
			url: resolve_url(app.hash ? decode_hash(new URL(location.href)) : location.href),
			replace_state: true,
			state: history_metadata?.persistState ? parse(history_metadata.state) : {},
			persist_state: history_metadata?.persistState ?? false
		});

		restore_reload_scroll();
	}

	_start_router();
}

async function _invalidate(reset_page_state = true) {
	// Accept all invalidations as they come, don't swallow any while another invalidation
	// is running because subsequent invalidations may make earlier ones outdated,
	// but batch multiple synchronous invalidations.
	await (pending_invalidate ||= Promise.resolve());
	if (pending_invalidate === null) return;
	pending_invalidate = null;

	const token = (invalidation_token = {});
	const nav_token = navigation_token;
	const prev_current = current;
	const intent = await get_navigation_intent(current.url, true);

	// Clear preload, it might be affected by the invalidation.
	// Also solves an edge case where a preload is triggered, the navigation for it
	// was then triggered and is still running while the invalidation kicks in,
	// at which point the invalidation should take over and "win".
	discard_load_cache();

	// Rerun queries
	/** @type {Map<string, Promise<void>>} */
	const live_query_reconnects = new Map();
	if (force_invalidation) {
		for (const [, { resource }] of cache_entries(query_map)) {
			void resource.refresh();
		}

		for (const [key, { resource }] of cache_entries(live_query_map)) {
			const promise = resource.reconnect();
			promise.catch(noop);
			live_query_reconnects.set(key, promise);
		}
	}

	const prev_state = page.state;
	const prev_shallow = page.shallow;
	const navigation_result = intent && (await load_route(intent));
	if (!navigation_result || token !== invalidation_token || nav_token !== navigation_token) {
		return;
	}

	if (navigation_result.type === 'redirect') {
		return _goto(
			new URL(navigation_result.location, current.url).href,
			{ replace: true },
			1,
			token
		);
	}

	// a navigation applied its result while the invalidation was loading,
	// so the invalidation contains outdated data for a page we are no longer on
	if (current !== prev_current) {
		return;
	}

	apply_navigation_result(navigation_result);

	// Preserve `page.state` when invalidating without resetting it (e.g. `refresh`/`refreshAll`).
	// Must run after `apply_navigation_result`, which overwrites `state`/`shallow` with the fresh
	// page object's `{}`/`null` values when the page changed.
	if (!reset_page_state) {
		update_page({ state: prev_state });
	}

	update_page({ shallow: prev_shallow });
	current = { ...navigation_result.state, nav: current.nav };
	reset_invalidation();

	// only wait for promises that are connected to queries that still exist
	/** @type {Promise<any>[]} */
	const promises = [];
	for (const [, { resource }] of cache_entries(query_map)) {
		promises.push(resource);
	}
	for (const [key] of cache_entries(live_query_map)) {
		const promise = live_query_reconnects.get(key);
		if (promise) {
			promises.push(promise);
		}
	}

	// Don't use allSettled yet because it's too new
	await Promise.all(promises).catch(noop);
}

function reset_invalidation() {
	invalidated.length = 0;
	force_invalidation = false;
}

let warned_snapshot_export = false;

/**
 * @param {number} index
 * @deprecated TODO 4.0 get rid of this
 */
function capture_snapshot(index) {
	if (props.components.some((c) => c?.snapshot)) {
		if (DEV && !warned_snapshot_export) {
			warned_snapshot_export = true;
			w.snapshot_export_deprecated();
		}
		snapshots[index] = props.components.map((c) => c?.snapshot?.capture());
	}
}

/**
 * @param {number} index
 * @deprecated TODO 4.0 get rid of this
 */
function restore_snapshot(index) {
	snapshots[index]?.forEach((value, i) => {
		props.components[i]?.snapshot?.restore(value);
	});
}

function persist_state() {
	capture_scroll(current_history_index);
	storage.set(HISTORY_INFO_KEY, history_info);

	capture_snapshot(current_navigation_index);
	storage.set(SNAPSHOT_KEY, snapshots);

	persist_navigation_snapshots(current_history_index);
}

/**
 * @param {string | URL} url
 * @param {{ type?: NavigationType; replace?: boolean; reset?: boolean; refreshAll?: boolean; invalidate?: Array<string | URL | ((url: URL) => boolean)>; state?: Record<string, any>; persistState?: boolean; event?: Event; action_result?: ActionResult }} [options]
 * @param {number} [redirect_count]
 * @param {{}} [nav_token]
 * @param {NavigationIntent | undefined} [intent] navigation intent, when already known by the caller (avoids recomputing it)
 * @returns {Promise<void>}
 */
export async function _goto(url, options = {}, redirect_count = 0, nav_token = {}, intent) {
	/** @type {Set<string>} */
	let query_keys;
	/** @type {Set<string>} */
	let live_query_keys;

	// Clear preload cache when refreshAll is true to ensure fresh data
	// after form submissions or explicit invalidations
	if (options.refreshAll) {
		discard_load_cache();
	}

	await navigate({
		type: options.type ?? 'goto',
		url: resolve_url(url),
		reset: options.reset,
		replace_state: options.replace,
		state: options.state,
		persist_state: options.persistState,
		event: options.event,
		action_result: options.action_result,
		redirect_count,
		nav_token,
		intent,
		accept: () => {
			if (options.refreshAll) {
				force_invalidation = true;
				query_keys = new Set();
				for (const [key, entry] of cache_entries(query_map)) {
					// don't refresh yet, as some queries will be unrendered,
					// but clear caches so that newly rendered queries
					// don't use stale data. TODO same for `live_query_map`
					entry.resource?.reset();
					query_keys.add(key);
				}
				live_query_keys = new Set();
				for (const [key] of cache_entries(live_query_map)) {
					live_query_keys.add(key);
				}
			}

			if (options.invalidate) {
				options.invalidate.forEach(push_invalidated);
			}
		}
	});

	if (options.refreshAll) {
		// TODO the ticks shouldn't be necessary, something inside Svelte itself is buggy
		// when a query in a layout that still exists after page change is refreshed earlier than this
		void tick()
			.then(tick)
			.then(() => {
				for (const [key, { resource }] of cache_entries(query_map)) {
					if (query_keys?.has(key)) {
						void resource.start();
					}
				}
				for (const [key, { resource }] of cache_entries(live_query_map)) {
					if (live_query_keys?.has(key)) {
						void resource.reconnect();
					}
				}
			});
	}
}

/** @param {import('./types.js').NavigationIntent} intent */
async function _preload_data(intent) {
	// Reuse the existing pending preload if it's for the same navigation.
	// Prevents an edge case where same preload is triggered multiple times,
	// then a later one is becoming the real navigation and the preload tokens
	// get out of sync.
	if (intent.id !== load_cache?.id) {
		discard_load_cache();

		const preload = {};
		preload_tokens.add(preload);
		load_cache = {
			id: intent.id,
			token: preload,
			promise: load_route({ ...intent, preload }).finally(() => {
				preload_tokens.delete(preload);
			}),
			fork: null
		};

		load_cache.promise.catch(discard_load_cache);

		if (__SVELTEKIT_FORK_PRELOADS__) {
			const lc = load_cache;

			lc.fork = lc.promise.then((result) => {
				// if load_cache was discarded before load_cache.promise could
				// resolve, bail rather than creating an orphan fork
				if (lc === load_cache && result.type === 'loaded') {
					try {
						return fork(() => {
							apply_navigation_result(result);
						});
					} catch {
						// if it errors, it's because the experimental flag isn't enabled in Svelte
					}
				}

				return null;
			});
		}
	}

	return load_cache.promise;
}

/**
 * Fetch and parse the route with the given ID from the server-side route resolution endpoint.
 * Returns `ENDPOINT_ONLY` if the route exists but has no code to preload, or `undefined` if
 * there is no such route. Only used when `router.resolution === 'server'`.
 * @param {string} id
 * @returns {Promise<import('types').CSRRoute | typeof ENDPOINT_ONLY | undefined>}
 */
async function load_route_by_id(id) {
	/** @type {{ route?: import('types').CSRRouteServer, endpoint_only?: boolean }} */
	let module;

	try {
		module = await import(
			/* @vite-ignore */
			base + route_id_resolution_pathname(id)
		);
	} catch {
		// if there's no module at that path the response is a 404 (or, on a static
		// host, fallback HTML with the wrong MIME type) and the import rejects —
		// treat it the same as an unknown route rather than surfacing a cryptic error
		return;
	}

	if (module.endpoint_only) {
		// The route exists, it just has no code to preload.
		route_id_cache.set(id, ENDPOINT_ONLY);
		return ENDPOINT_ONLY;
	}

	if (!module.route) return;

	return parse_and_cache_server_route(module.route);
}

/**
 * Import the modules for a route's layout and leaf nodes, without running `load` functions.
 * @param {import('types').CSRRoute} route
 * @returns {Promise<void>}
 */
async function load_route_nodes(route) {
	await Promise.all(
		/** @type {[has_server_load: boolean, node_loader: import('types').CSRPageNodeLoader][]} */ (
			[...route.layouts, route.leaf].filter(Boolean)
		).map(([, node_loader]) => node_loader())
	);
}

/**
 * @param {URL} url
 * @returns {Promise<void>}
 */
async function _preload_code(url) {
	const route = (await get_navigation_intent(url, false))?.route;

	if (route) {
		await load_route_nodes(route);
	}
}

/**
 * @param {import('./types.js').NavigationFinished} result
 * @param {HTMLElement} target
 * @param {boolean} should_hydrate
 */
async function initialize(result, target, should_hydrate) {
	if (__SVELTEKIT_DEV__ && result.state.error && document.querySelector('vite-error-overlay'))
		return;

	/** @type {NavigationEvent} */
	const nav = {
		params: current.params,
		route: { id: current.route?.id ?? null },
		url: new URL(location.href)
	};

	current = {
		...result.state,
		nav
	};

	// Removes the style node we used to avoid FOUC during development
	if (__SVELTEKIT_DEV__) {
		const style = document.querySelector('style[data-sveltekit]');
		if (style) style.remove();
	}

	apply_navigation_result(result);

	// TODO treeshake `hydrate` in csr mode
	const render = should_hydrate ? hydrate : mount;

	render(Root, {
		target,
		props,
		transformError: /** @param {unknown} e */ async (e) => {
			const error = await handle_error(e, current.nav);
			update_page({ error, status: error.status });
			return error;
		}
	});

	// Wait for a microtask in case svelte experimental async is enabled,
	// which causes component script blocks to run asynchronously
	void (await Promise.resolve());

	if (should_hydrate) {
		/** @type {AfterNavigate} */
		const navigation = {
			from: null,
			to: {
				...nav,
				scroll: history_info[current_history_index]?.scroll ?? scroll_state()
			},
			willUnload: false,
			type: 'enter',
			shallow: false,
			complete: Promise.resolve()
		};

		after_navigate_callbacks.forEach((fn) => fn(navigation));
	}

	restore_snapshot(current_navigation_index);
	restore_navigation_snapshot(current_history_index);

	started = true;
}

/**
 *
 * @param {{
 *   url: URL;
 *   params: Record<string, string>;
 *   branch: Array<import('./types.js').BranchNode | undefined>;
 *   errors?: Array<import('types').CSRPageNodeLoader | undefined>;
 *   status?: number;
 *   error: App.Error | null;
 *   route: import('types').CSRRoute | null;
 *   form?: Record<string, any> | null;
 * }} opts
 */
async function get_navigation_result_from_branch({
	url,
	params,
	branch,
	errors,
	status,
	error,
	route,
	form
}) {
	/** @type {import('types').TrailingSlash} */
	let slash = 'never';

	// if `paths.base === '/a/b/c`, then the root route is always `/a/b/c/`, regardless of
	// the `trailingSlash` route option, so that relative paths to JS and CSS work
	if (base && (url.pathname === base || url.pathname === base + '/')) {
		slash = 'always';
	} else {
		for (const node of branch) {
			if (node?.slash !== undefined) slash = node.slash;
		}
	}

	url.pathname = normalize_path(url.pathname, slash);
	// eslint-disable-next-line no-self-assign
	url.search = url.search; // turn `/?` into `/`

	/** @type {import('./types.js').NavigationFinished} */
	const result = {
		type: 'loaded',
		state: {
			url,
			params,
			branch,
			error,
			route
		},
		props: {
			page: { ...page },
			tree: /** @type {RenderNode} */ ({})
		}
	};

	if (error) {
		result.props.error = error;
	}

	if (form !== undefined) {
		result.props.form = form;
	}

	let data = {};
	let data_changed = !page;

	const error_components =
		errors &&
		(await build_error_chain(branch, errors, (loader) => loader().then((e) => e.component)));

	let current_node = result.props.tree;
	let current_depth = 1;

	/** @type {RenderNode | undefined} */
	let previous_node = props.tree;

	for (let i = 0; i < branch.length; i += 1) {
		const node = branch[i];
		const prev = current.branch[i];

		if (!node) continue;

		if (
			// if an ancestor node in this path changed, `data_changed` is already true and the
			// accumulated `data` differs from the previous render, so we must re-merge
			data_changed ||
			!previous_node ||
			node?.data !== prev?.data
		) {
			current_node.data = { ...data, ...node.data };
			data_changed = true;
		} else {
			// use existing object — prevents effects re-running unnecessarily
			current_node.data = previous_node.data;
		}

		data = current_node.data;

		if (i < branch.length - 1) {
			let next_index = i + 1;
			while (next_index < branch.length && !branch[next_index]) {
				next_index += 1;
			}

			const next = branch[next_index];

			if (next) {
				current_node = current_node.child = new RenderNode(
					next.node.component,
					error_components?.[current_depth++]
				);

				previous_node = previous_node?.child;
			}
		}
	}

	const page_changed =
		!current.url ||
		url.href !== current.url.href ||
		current.error !== error ||
		(form !== undefined && form !== page.form) ||
		data_changed;

	if (page_changed) {
		result.props.page = {
			error,
			params,
			route: {
				id: route?.id ?? null
			},
			state: {},
			shallow: null,
			status: status ?? error?.status ?? 200,
			url: new URL(url),
			form: form ?? null,
			data
		};
	}

	return result;
}

/**
 * Call the universal load function of the given node, if it exists.
 *
 * @param {{
 *   loader: import('types').CSRPageNodeLoader;
 * 	 parent: () => Promise<Record<string, any>>;
 *   url: URL;
 *   params: Record<string, string>;
 *   route: { id: string | null };
 * 	 server_data_node: import('./types.js').DataNode | null;
 * }} options
 * @returns {Promise<import('./types.js').BranchNode>}
 */
async function load_node({ loader, parent, url, params, route, server_data_node }) {
	/** @type {Record<string, any> | null} */
	let data = null;

	let is_tracking = true;

	/** @type {import('types').Uses} */
	const uses = {
		dependencies: new Set(),
		params: new Set(),
		parent: false,
		route: false,
		url: false,
		search_params: new Set()
	};

	const node = await loader();

	if (DEV) {
		validate_page_exports(node.universal);

		if (node.universal && app.hash) {
			const options = Object.keys(node.universal).filter((o) => o !== 'load');

			if (options.length > 0) {
				shared_errors.router_hash_page_options({
					source: /** @type {string} */ (route.id),
					options: options.map((o) => `'${o}'`).join(', ')
				});
			}
		}
	}

	if (__SVELTEKIT_HAS_UNIVERSAL_LOAD__ && node.universal?.load) {
		/** @param {string[]} deps */
		function depends(...deps) {
			for (const dep of deps) {
				if (DEV) validate_depends(/** @type {string} */ (route.id), dep);

				const { href } = new URL(dep, url);
				uses.dependencies.add(href);
			}
		}

		/** @type {LoadEvent} */
		const load_input = {
			tracing: { enabled: false, root: noop_span, current: noop_span },
			route: new Proxy(route, {
				get: (target, key) => {
					if (is_tracking) {
						uses.route = true;
					}
					return target[/** @type {'id'} */ (key)];
				}
			}),
			params: new Proxy(params, {
				get: (target, key) => {
					if (is_tracking) {
						uses.params.add(/** @type {string} */ (key));
					}
					return target[/** @type {string} */ (key)];
				}
			}),
			data: server_data_node?.data ?? null,
			url: make_trackable(
				url,
				() => {
					if (is_tracking) {
						uses.url = true;
					}
				},
				(param) => {
					if (is_tracking) {
						uses.search_params.add(param);
					}
				},
				app.hash
			),
			async fetch(resource, init) {
				if (resource instanceof Request) {
					// we're not allowed to modify the received `Request` object, so in order
					// to fixup relative urls we create a new equivalent `init` object instead
					init = {
						// the request body must be consumed in memory until browsers
						// implement streaming request bodies and/or the body getter
						body:
							resource.method === 'GET' || resource.method === 'HEAD'
								? undefined
								: await resource.blob(),
						cache: resource.cache,
						credentials: resource.credentials,
						// the server sets headers to `undefined` if there are no headers but
						// the client defaults to an empty Headers object in the Request object.
						// To keep the two values in sync, we explicitly set the headers to `undefined`.
						// Also, not sure why, but sometimes 0 is evaluated as truthy so we need to
						// explicitly compare the headers length to a number here
						headers: [...resource.headers].length > 0 ? resource?.headers : undefined,
						integrity: resource.integrity,
						keepalive: resource.keepalive,
						method: resource.method,
						mode: resource.mode,
						redirect: resource.redirect,
						referrer: resource.referrer,
						referrerPolicy: resource.referrerPolicy,
						signal: resource.signal,
						...init
					};
				}

				const { resolved, promise } = resolve_fetch_url(resource, init, url);

				if (is_tracking) {
					depends(resolved.href);
				}

				return promise;
			},
			setHeaders: noop,
			depends,
			parent() {
				if (is_tracking) {
					uses.parent = true;
				}
				return parent();
			},
			untrack(fn) {
				is_tracking = false;
				try {
					return fn();
				} finally {
					is_tracking = true;
				}
			}
		};

		if (DEV) {
			try {
				lock_fetch();
				data = (await node.universal.load.call(null, load_input)) ?? null;
				validate_load_response(data, `related to route '${route.id}'`);
			} finally {
				unlock_fetch();
			}
		} else {
			data = (await node.universal.load.call(null, load_input)) ?? null;
		}
	}

	return {
		node,
		loader,
		server: server_data_node,
		universal: node.universal?.load ? { type: 'data', data, uses } : null,
		data: data ?? server_data_node?.data ?? null,
		slash: node.universal?.trailingSlash ?? server_data_node?.slash
	};
}

/**
 * @param {Request | string | URL} input
 * @param {RequestInit | undefined} init
 * @param {URL} url
 */
function resolve_fetch_url(input, init, url) {
	// we must fixup relative urls so they are resolved from the target page
	const resolved = new URL(input instanceof Request ? input.url : input, url);

	const requested = fetch_cache_url(resolved, url);

	const promise = started
		? subsequent_fetch(requested, resolved.href, init)
		: initial_fetch(requested, init);

	return { resolved, promise };
}

/**
 * @param {boolean} parent_changed
 * @param {boolean} route_changed
 * @param {boolean} url_changed
 * @param {Set<string>} search_params_changed
 * @param {import('types').Uses | undefined} uses
 * @param {Record<string, string>} params
 */
function has_changed(
	parent_changed,
	route_changed,
	url_changed,
	search_params_changed,
	uses,
	params
) {
	if (force_invalidation) return true;

	if (!uses) return false;

	if (uses.parent && parent_changed) return true;
	if (uses.route && route_changed) return true;
	if (uses.url && url_changed) return true;

	for (const tracked_params of uses.search_params) {
		if (search_params_changed.has(tracked_params)) return true;
	}

	for (const param of uses.params) {
		if (params[param] !== current.params[param]) return true;
	}

	for (const href of uses.dependencies) {
		if (invalidated.some((fn) => fn(new URL(href)))) return true;
	}

	return false;
}

/**
 * @param {import('types').ServerDataNode | import('types').ServerDataSkippedNode | null} node
 * @param {import('./types.js').DataNode | null} [previous]
 * @returns {import('./types.js').DataNode | null}
 */
function create_data_node(node, previous) {
	if (node?.type === 'data') return node;
	if (node?.type === 'skip') return previous ?? null;
	return null;
}

/**
 * @param {URL | null} old_url
 * @param {URL} new_url
 */
function diff_search_params(old_url, new_url) {
	if (!old_url) return new Set(new_url.searchParams.keys());

	const changed = new Set([...old_url.searchParams.keys(), ...new_url.searchParams.keys()]);

	for (const key of changed) {
		const old_values = old_url.searchParams.getAll(key).sort();
		const new_values = new_url.searchParams.getAll(key).sort();

		if (
			old_values.length === new_values.length &&
			old_values.every((value, i) => value === new_values[i])
		) {
			changed.delete(key);
		}
	}

	return changed;
}

/**
 * @overload
 * @param {import('./types.js').NavigationIntent & { action_result?: ActionResult }} intent
 * @returns {Promise<import('./types.js').NavigationResult | undefined>}
 */
/**
 * @overload
 * @param {import('./types.js').NavigationIntent & { preload: {} }} intent
 * @returns {Promise<import('./types.js').NavigationResult>}
 */
/**
 * @param {import('./types.js').NavigationIntent & { preload?: {}; action_result?: ActionResult }} intent
 * @returns {Promise<import('./types.js').NavigationResult | undefined>}
 */
async function load_route({ id, invalidating, url, params, route, preload, action_result }) {
	if (!action_result && load_cache?.id === id) {
		// the preload becomes the real navigation
		preload_tokens.delete(load_cache.token);
		return load_cache.promise;
	}

	const { errors, layouts, leaf } = route;

	const loaders = [...layouts, leaf];
	const leaf_index = loaders.length - 1;
	const action_error = action_result?.type === 'error';

	// preload modules to avoid waterfall, but handle rejections
	// so they don't get reported to Sentry et al (we don't need
	// to act on the failures at this point)
	errors.forEach((loader) => loader?.().catch(noop));
	loaders.forEach((loader) => loader?.[1]().catch(noop));

	/** @type {import('types').ServerNodesResponse | import('types').ServerRedirectNode | null} */
	let server_data = null;
	const url_changed = current.url ? id !== get_page_key(current.url) : false;
	// current.route is null after an error-page render, so a missing route counts as changed
	const route_changed = !current.route || route.id !== current.route.id;
	const search_params_changed = diff_search_params(current.url, url);

	let parent_invalid = false;

	if (__SVELTEKIT_HAS_SERVER_LOAD__) {
		const invalid_server_nodes = loaders.map((loader, i) => {
			if (action_error && i === leaf_index) return false;

			const previous = current.branch[i];

			const invalid =
				!!loader?.[0] &&
				(previous?.loader !== loader[1] ||
					has_changed(
						parent_invalid,
						route_changed,
						url_changed,
						search_params_changed,
						previous.server?.uses,
						params
					));

			if (invalid) {
				// For the next one
				parent_invalid = true;
			}

			return invalid;
		});

		if (invalid_server_nodes.some(Boolean)) {
			try {
				server_data = await load_data(url, invalid_server_nodes);
			} catch (error) {
				const handled_error = await handle_error(error, { url, params, route: { id } });

				if (preload && preload_tokens.has(preload)) {
					throw handled_error;
				}

				return load_root_error_page({
					error: handled_error,
					url,
					route
				});
			}

			if (server_data.type === 'redirect') {
				return server_data;
			}
		}
	}

	const server_data_nodes = server_data?.nodes;

	let parent_changed = false;

	const branch_promises = loaders.map(async (loader, i) => {
		if (!loader || (action_error && i === leaf_index)) return;

		/** @type {import('./types.js').BranchNode | undefined} */
		const previous = current.branch[i];

		const server_data_node = server_data_nodes?.[i];

		// reuse data from previous load if it's still valid
		const valid =
			(!server_data_node || server_data_node.type === 'skip') &&
			loader[1] === previous?.loader &&
			!has_changed(
				parent_changed,
				route_changed,
				url_changed,
				search_params_changed,
				previous.universal?.uses,
				params
			);
		if (valid) return previous;

		parent_changed = true;

		if (server_data_node?.type === 'error') {
			// rethrow and catch below
			throw new HandledHttpError(server_data_node.error);
		}

		return load_node({
			loader: loader[1],
			url,
			params,
			route,
			parent: async () => {
				const data = {};
				for (let j = 0; j < i; j += 1) {
					Object.assign(data, (await branch_promises[j])?.data);
				}
				return data;
			},
			server_data_node: create_data_node(
				// server_data_node is undefined if it wasn't reloaded from the server;
				// and if current loader uses server data, we want to reuse previous data.
				server_data_node === undefined && loader[0] ? { type: 'skip' } : (server_data_node ?? null),
				loader[0] ? previous?.server : undefined
			)
		});
	});

	// if we don't do this, rejections will be unhandled
	for (const p of branch_promises) p.catch(noop);

	/** @type {Array<import('./types.js').BranchNode | undefined>} */
	const branch = [];

	for (let i = 0; i < loaders.length; i += 1) {
		if (loaders[i]) {
			try {
				branch.push(await branch_promises[i]);
			} catch (err) {
				if (err instanceof Redirect) {
					return {
						type: 'redirect',
						status: err.status,
						location: err.location
					};
				}

				if (preload && preload_tokens.has(preload)) {
					throw await handle_error(err, { params, url, route: { id: route.id } });
				}

				/** @type {App.Error} */
				let error;

				if (server_data_nodes?.includes(/** @type {import('types').ServerErrorNode} */ (err))) {
					// this is the server error rethrown above, reconstruct but don't invoke
					// the client error handler; it should've already been handled on the server
					error = /** @type {import('types').ServerErrorNode} */ (err).error;
				} else if (err instanceof HttpError) {
					error = await handle_error(err, { params, url, route: { id: route.id } });
				} else {
					// Referenced node could have been removed due to redeploy, check
					if (await updated.check()) {
						// Before reloading, try to update the service worker if it exists
						await update_service_worker();
						return await native_navigation(url);
					}

					error = await handle_error(err, { params, url, route: { id: route.id } });
				}

				return load_route_error({
					i,
					branch,
					errors,
					error,
					url,
					params,
					route
				});
			}
		} else {
			// push an empty slot so we can rewind past gaps to the
			// layout that corresponds with an +error.svelte page
			branch.push(undefined);
		}
	}

	if (action_result?.type === 'error') {
		// render the nearest error boundary of the route the action belongs to,
		// as a native form submission would
		return load_route_error({
			i: loaders.length,
			branch,
			errors,
			error: action_result.error,
			status: action_result.status,
			url,
			params,
			route
		});
	}

	return get_navigation_result_from_branch({
		url,
		params,
		branch,
		errors,
		error: null,
		route,
		status: action_result?.status,
		// `error` results returned above, `redirect` results are never passed here
		form:
			action_result?.type === 'success' || action_result?.type === 'failure'
				? (action_result.data ?? null)
				: // Reset `form` on navigation, but not invalidation
					invalidating
					? undefined
					: null
	});
}

/**
 * @param {number} i Start index to backtrack from
 * @param {Array<import('./types.js').BranchNode | undefined>} branch Branch to backtrack
 * @param {Array<import('types').CSRPageNodeLoader | undefined>} errors All error pages for this branch
 * @returns {Promise<Array<import('./types.js').BranchNode | undefined> | undefined>} the branch truncated at the error page's depth
 */
async function load_nearest_error_page(i, branch, errors) {
	for (const { error, idx } of nearest_error_pages(i, branch, errors)) {
		try {
			return branch.slice(0, idx).concat({
				node: await error(),
				loader: error,
				data: {},
				server: null,
				universal: null
			});
		} catch {
			continue;
		}
	}
}

/**
 * @param {{
 *   i: number;
 *   branch: Array<import('./types.js').BranchNode | undefined>;
 *   errors: Array<import('types').CSRPageNodeLoader | undefined>;
 *   error: App.Error;
 *   status?: number;
 *   url: URL;
 *   params: Record<string, string>;
 *   route: import('./types.js').NavigationIntent['route'];
 * }} opts
 */
async function load_route_error({ i, branch, errors, error, status, url, params, route }) {
	const error_branch = await load_nearest_error_page(i, branch, errors);
	if (error_branch) {
		return get_navigation_result_from_branch({
			url,
			params,
			branch: error_branch,
			errors,
			error,
			status,
			route
		});
	}

	return server_fallback(url, { id: route.id }, error);
}

/**
 * @param {{
 *   error: App.Error;
 *   url: URL;
 *   route: { id: string | null }
 * }} opts
 * @returns {Promise<import('./types.js').NavigationFinished | undefined>} returns `undefined` in case of a redirect
 */
async function load_root_error_page({ error, url, route }) {
	/** @type {Record<string, string>} */
	const params = {}; // error page does not have params

	/** @type {import('types').ServerDataNode | null} */
	let server_data_node = null;

	if (__SVELTEKIT_HAS_SERVER_LOAD__) {
		const default_layout_has_server_load = app.server_loads[0] === 0;

		if (default_layout_has_server_load) {
			// TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use
			// existing root layout data
			try {
				const server_data = await load_data(url, [true]);

				if (
					server_data.type !== 'data' ||
					(server_data.nodes[0] && server_data.nodes[0].type !== 'data')
				) {
					throw 0;
				}

				server_data_node = server_data.nodes[0] ?? null;
			} catch (e) {
				// at this point we have no choice but to fall back to the server, if it wouldn't
				// bring us right back here, turning this into an endless loop.
				// if __data.json returned 404, the route doesn't exist — don't reload or we loop
				if (
					!(e instanceof HttpError && e.status === 404) &&
					(url.origin !== origin || url.pathname !== location.pathname || hydrated)
				) {
					return await native_navigation(url);
				}
			}
		}
	}

	try {
		const root_layout = await load_node({
			loader: default_layout_loader,
			url,
			params,
			route,
			parent: () => Promise.resolve({}),
			server_data_node: create_data_node(server_data_node)
		});

		/** @type {import('./types.js').BranchNode} */
		const root_error = {
			node: await default_error_loader(),
			loader: default_error_loader,
			universal: null,
			server: null,
			data: null
		};

		return get_navigation_result_from_branch({
			url,
			params,
			branch: [root_layout, root_error],
			error,
			errors: [],
			route: null
		});
	} catch (error) {
		// client-side navigation if the root layout loader throws a redirect while
		// rendering the default error page
		if (error instanceof Redirect) {
			await _goto(new URL(error.location, location.href));
			return;
		}

		// otherwise, render the static error page
		const error_template = await app.get_error_template();
		const handled = await handle_error(error, { url, params, route });
		const message = String(handled?.message ?? '')
			.replace(/&/g, '&amp;')
			.replace(/</g, '&lt;')
			.replace(/>/g, '&gt;');
		const html = error_template({ status: handled.status, message });
		const parsed = new DOMParser().parseFromString(html, 'text/html');
		document.documentElement.replaceChild(document.adoptNode(parsed.head), document.head);
		document.documentElement.replaceChild(document.adoptNode(parsed.body), document.body);

		throw error;
	}
}

/**
 * Resolve the relative rerouted URL for a client-side navigation
 * @param {URL} url
 * @returns {Promise<URL | undefined>}
 */
async function get_rerouted_url(url) {
	const href = url.href;

	if (reroute_cache.has(href)) {
		return reroute_cache.get(href);
	}

	let rerouted;

	try {
		const promise = (async () => {
			// reroute could alter the given URL, so we pass a copy
			let rerouted =
				(await app.hooks.reroute({
					url: new URL(url),
					fetch: async (input, init) => {
						return resolve_fetch_url(input, init, url).promise;
					}
				})) ?? url;

			if (typeof rerouted === 'string') {
				const tmp = new URL(url); // do not mutate the incoming URL

				if (app.hash) {
					tmp.hash = rerouted;
				} else {
					tmp.pathname = rerouted;
				}

				rerouted = tmp;
			}

			return rerouted;
		})();

		reroute_cache.set(href, promise);
		rerouted = await promise;
	} catch (e) {
		reroute_cache.delete(href);
		if (DEV) {
			// in development, print the error...
			console.error(e);

			// ...and pause execution, since otherwise we will immediately reload the page
			debugger; // eslint-disable-line
		}

		// fall back to native navigation
		return;
	}

	return rerouted;
}

/**
 * Resolve the full info (which route, params, etc.) for a client-side navigation from the URL,
 * taking the reroute hook into account. If this isn't a client-side-navigation (or the URL is undefined),
 * returns undefined.
 * @param {URL | undefined} url
 * @param {boolean} invalidating
 * @returns {Promise<import('./types.js').NavigationIntent | undefined>}
 */
export async function get_navigation_intent(url, invalidating) {
	if (!url) return;
	if (is_external_url(url, base, app.hash)) return;

	if (__SVELTEKIT_CLIENT_ROUTING__) {
		const rerouted = await get_rerouted_url(url);
		if (!rerouted) return;

		const path = get_url_path(rerouted);

		for (const route of routes) {
			const params = route.exec(path);

			if (params) {
				return {
					id: get_page_key(url),
					invalidating,
					route,
					params,
					url
				};
			}
		}
	} else {
		/** @type {{ route?: import('types').CSRRouteServer, params: Record<string, string>}} */
		const { route, params } = await import(
			/* @vite-ignore */
			add_resolution_suffix(url.pathname)
		);

		if (!route) return;

		return {
			id: get_page_key(url),
			invalidating,
			route: parse_and_cache_server_route(route),
			params,
			url
		};
	}
}

/** @param {URL} url */
function get_url_path(url) {
	return (
		decode_pathname(
			app.hash ? url.hash.replace(/^#/, '').replace(/[?#].+/, '') : url.pathname.slice(base.length)
		) || '/'
	);
}

/** @param {URL} url */
function get_page_key(url) {
	return (app.hash ? url.hash.replace(/^#/, '') : url.pathname) + url.search;
}

/**
 * @param {{
 *   url: URL;
 *   type: Navigation["type"];
 *   intent?: import('./types.js').NavigationIntent;
 *   delta?: number;
 *   event?: PopStateEvent | MouseEvent;
 *   scroll?: { x: number, y: number };
 *   shallow?: boolean;
 *   target?: { params: Record<string, string> | null; route: { id: string } | null; url: URL };
 * }} opts
 */
function _before_navigate({ url, type, intent, delta, event, scroll, shallow = false, target }) {
	let should_block = false;

	const nav = create_navigation(current, intent, url, type, scroll ?? null, shallow, target);

	if (nav.navigation.type === 'popstate' && delta !== undefined) {
		nav.navigation.delta = delta;
	}

	if (event !== undefined) {
		// @ts-ignore
		nav.navigation.event = event;
	}

	const cancellable = {
		...nav.navigation,
		cancel: () => {
			should_block = true;
			nav.reject(new Error('navigation cancelled'));
		}
	};

	if (!is_navigating) {
		// Don't run the event during redirects
		// TODO this isn't fully right: if you do a goto(...) while another goto(...) is in progress,
		// or you click a link while a navigation is in progress, the beforNavigate calls are not triggered,
		// and maybe they should be?
		before_navigate_callbacks.forEach((fn) => fn(cancellable));
	}

	return should_block ? null : nav;
}

/**
 * @param {{
 *   type: NavigationType;
 *   url: URL;
 *   popped?: {
 *     state: Record<string, any>;
 *     scroll?: { x: number, y: number };
 *     delta: number;
 *     shallow: { params: Record<string, string> | null; route: { id: string } | null; url: URL } | null;
 *   };
 *   reset?: boolean;
 *   replace_state?: boolean;
 *   state?: Record<string, any>;
 *   persist_state?: boolean;
 *   redirect_count?: number;
 *   nav_token?: {};
 *   accept?: () => void;
 *   block?: () => void;
 *   event?: Event;
 *   intent?: NavigationIntent | undefined;
 *   action_result?: ActionResult;
 * }} opts
 * @returns {Promise<void>}
 */
async function navigate({
	type,
	url,
	popped,
	reset = true,
	replace_state,
	state = {},
	persist_state = false,
	redirect_count = 0,
	nav_token = {},
	accept = noop,
	block = noop,
	event,
	intent,
	action_result
}) {
	const prev_token = navigation_token;
	const prev_invalidation_token = invalidation_token;
	navigation_token = invalidation_token = nav_token;

	intent ??= await get_navigation_intent(url, false);
	const nav =
		type === 'enter'
			? create_navigation(current, intent, url, type)
			: _before_navigate({
					url,
					type,
					delta: popped?.delta,
					intent,
					scroll: popped?.scroll,
					shallow: !!popped?.shallow,
					target: popped?.shallow ?? undefined,
					// @ts-ignore
					event
				});

	if (!nav) {
		block();
		if (navigation_token === nav_token) navigation_token = prev_token;
		if (invalidation_token === nav_token) invalidation_token = prev_invalidation_token;
		return;
	}

	// store this before calling `accept()`, which may change the index
	const previous_history_index = current_history_index;
	const previous_navigation_index = current_navigation_index;
	const previous_snapshot_registrations = current_registrations();

	accept();

	is_navigating = true;

	if (started && nav.navigation.type !== 'enter') {
		set_navigation(nav.navigation);
	}

	let navigation_result = intent && (await load_route({ ...intent, action_result }));

	if (!navigation_result) {
		const external = is_external_url(url, base, app.hash);
		// Special case for hash mode during DEV: If someone accidentally forgets to use a hash for the link,
		// they would end up here in an endless loop. Fall back to error page in that case
		const missing_hash = external && DEV && app.hash;

		if (external && !missing_hash) {
			return await native_navigation(url, replace_state);
		}

		navigation_result = await server_fallback(
			url,
			{ id: null },
			await handle_error(
				new SvelteKitError(
					404,
					'Not Found',
					`Not found: ${url.pathname}${missing_hash ? ' (did you forget the hash?)' : ''}`
				),
				{ url, params: {}, route: { id: null } }
			),
			replace_state
		);
	}

	// if this is an internal navigation intent, use the normalized
	// URL for the rest of the function
	url = intent?.url || url;

	// abort if user navigated during update
	if (navigation_token !== nav_token) {
		nav.reject(new Error('navigation aborted'));
		return;
	}

	if (!navigation_result) return;

	if (navigation_result.type === 'redirect') {
		// whatwg fetch spec https://fetch.spec.whatwg.org/#http-redirect-fetch says to error after 20 redirects
		if (redirect_count < 20) {
			// a preloaded redirect has been consumed; a later hop back to this route must load afresh
			if (load_cache?.id === intent?.id) discard_load_cache();

			await navigate({
				type,
				url: new URL(navigation_result.location, url),
				popped,
				reset,
				replace_state,
				state,
				persist_state,
				redirect_count: redirect_count + 1,
				nav_token
			});

			nav.fulfil(undefined);
			return;
		}

		navigation_result = await load_root_error_page({
			error: await handle_error(
				capture_error(() => e.redirect_loop({ url: url.href })),
				{
					url,
					params: {},
					route: { id: null }
				}
			),
			url,
			route: { id: null }
		});

		if (!navigation_result) return;
	} else if (/** @type {number} */ (navigation_result.props.page.status) >= 400) {
		if (await updated.check()) {
			// Before reloading, try to update the service worker if it exists
			await update_service_worker();
			return await native_navigation(url, replace_state);
		}
	}

	// reset invalidation only after a finished navigation. If there are redirects or
	// additional invalidations, they should get the same invalidation treatment
	reset_invalidation();

	updating = true;

	if (!popped) {
		// popstate captures the source entry synchronously, before the traversal is resolved
		capture_scroll(previous_history_index);
		capture_snapshot(previous_navigation_index);
		if (replace_state) {
			delete_navigation_snapshot(previous_history_index);
		} else {
			capture_navigation_snapshot(previous_history_index);
		}
	}

	// ensure the url pathname matches the page's trailing slash option
	if (navigation_result.props.page.url.pathname !== url.pathname) {
		url.pathname = navigation_result.props.page.url.pathname;
	}

	if (popped) {
		state = popped.state;
	} else {
		// we immediately serialize-then-parse to ensure that the value is
		// serializable, and to prevent the developer from dangerously
		// relying on the identity of the serialized objects
		const serialized_state = stringify(state);
		state = parse(serialized_state);

		// Store the serialized state so the browser history can preserve custom transport values.
		// This is a new navigation, rather than a popstate.
		const change = replace_state ? 0 : 1;
		if (type !== 'enter') {
			if (reset) current_reset_index += 1;
		}

		const entry = {
			[HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
				historyIndex: (current_history_index += change),
				navigationIndex: (current_navigation_index += change),
				state: serialized_state,
				persistState: persist_state,
				resetIndex: current_reset_index
			})
		};

		const fn = replace_state ? history.replaceState : history.pushState;
		fn.call(history, entry, '', url);
		set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);

		if (!replace_state) {
			clear_onward_history(current_history_index, current_navigation_index);
		}
	}

	// also compare ids to avoid using wrong fork (e.g. a new one could've been added while navigating)
	const load_cache_fork = intent && load_cache?.id === intent.id ? load_cache.fork : null;
	// reset preload synchronously after the history state has been set to avoid race conditions
	if (load_cache?.fork && !load_cache_fork) {
		// discard fork of different route
		discard_load_cache();
	} else {
		load_cache = null;
		current_a = { element: undefined, href: undefined };
	}

	navigation_result.props.page.state = state;
	navigation_result.props.page.shallow = popped?.shallow ?? null;

	/**
	 * @type {Promise<void> | undefined}
	 */
	let commit_promise;
	if (started) {
		await run_on_navigate_callbacks(/** @type {OnNavigate} */ (nav.navigation));

		// Type-casts are save because we know this resolved a proper SvelteKit route
		const target = popped?.shallow
			? {
					params: navigation_result.state.params,
					route: { id: navigation_result.state.route?.id ?? null },
					url: navigation_result.state.url
				}
			: /** @type {NavigationTarget} */ (nav.navigation.to);
		current = {
			...navigation_result.state,
			nav: {
				params: /** @type {Record<string, any>} */ (target.params),
				route: target.route,
				url: target.url
			}
		};

		// reset url before updating page store
		if (navigation_result.props.page) {
			navigation_result.props.page.url = url;
		}

		// Remove focus before updating the component tree, so that blur/focusout
		// handlers fire while the old component's data is still valid (#14575)
		blur_active_element(reset);

		const fork = load_cache_fork && (await load_cache_fork);

		if (fork) {
			commit_promise = fork.commit();
			// `fork.commit()` applies the preloaded state synchronously before the
			// first `await`, so reset any previously-failed boundaries now so the
			// stale `+error.svelte` is torn down. See sveltejs/kit#15694.
			for (const reset_boundary of resetters) {
				reset_boundary();
			}
			resetters.clear();
		} else {
			apply_navigation_result(navigation_result);

			// Reset boundaries that failed on a previous navigation once the new props have
			// flushed (see sveltejs/kit#15694). Resetting first re-renders the old content at
			// a depth the new tree may not have, stranding the stale `+error.svelte`.
			commit_promise = settled().then(() => {
				for (const reset_boundary of resetters) {
					reset_boundary();
				}
				resetters.clear();
			});
		}

		has_navigated = true;
	} else {
		await initialize(navigation_result, target, false);
	}

	const finished = await finish_navigation(
		nav,
		nav_token,
		url,
		popped?.scroll,
		reset,
		commit_promise
	);
	if (!finished) return;

	if (type === 'popstate') {
		restore_snapshot(current_navigation_index);
	}
	// new and replaced entries have no stored values, so this only resets there
	restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);

	set_navigation(null);

	updating = false;
}

/**
 * Runs the `onNavigate` callbacks and registers any functions they return to run after the navigation
 * @param {OnNavigate} navigation
 */
async function run_on_navigate_callbacks(navigation) {
	const after_navigate = (
		await Promise.all(
			// eslint-disable-next-line @typescript-eslint/await-thenable -- we need to await because they can be asynchronous
			Array.from(on_navigate_callbacks, (fn) => fn(navigation))
		)
	).filter(/** @returns {value is () => void} */ (value) => typeof value === 'function');

	if (after_navigate.length > 0) {
		function cleanup() {
			after_navigate.forEach((fn) => after_navigate_callbacks.delete(fn));
		}

		after_navigate.push(cleanup);
		after_navigate.forEach((fn) => after_navigate_callbacks.add(fn));
	}
}

/**
 * Settles the navigation once `updated` resolves. Returns `false` if a newer navigation
 * superseded it in the meantime, in which case the caller must stop
 * @param {ReturnType<typeof create_navigation>} nav
 * @param {{}} nav_token
 * @param {URL} url
 * @param {{ x: number, y: number } | null | undefined} popped_scroll the scroll position to restore for popstate navigations
 * @param {boolean} reset
 * @param {Promise<void> | undefined} updated
 */
async function finish_navigation(nav, nav_token, url, popped_scroll, reset, updated) {
	await updated;

	if (navigation_token !== nav_token) {
		nav.reject(new Error('navigation aborted'));
		return false;
	}

	restore_scroll(url, reset, popped_scroll);
	if (reset && document.activeElement === document.body) {
		reset_focus(url);
	}

	is_navigating = false;

	nav.fulfil(undefined);

	// Update to.scroll to the actual scroll position after navigation completed
	if (nav.navigation.to) {
		nav.navigation.to.scroll = scroll_state();
	}

	after_navigate_callbacks.forEach((fn) => fn(/** @type {AfterNavigate} */ (nav.navigation)));

	return true;
}

/**
 * Does a full page reload if it wouldn't result in an endless loop in the SPA case
 * @param {URL} url
 * @param {{ id: string | null }} route
 * @param {App.Error} error
 * @param {boolean} [replace_state]
 * @returns {Promise<import('./types.js').NavigationFinished | undefined>}
 */
async function server_fallback(url, route, error, replace_state) {
	if (url.origin === origin && url.pathname === location.pathname && !hydrated) {
		// We would reload the same page we're currently on, which isn't hydrated,
		// which means no SSR, which means we would end up in an endless loop
		return await load_root_error_page({
			error,
			url,
			route
		});
	}

	if (DEV && error.status !== 404) {
		w.full_reload_after_error();

		debugger; // eslint-disable-line
	}

	return await native_navigation(url, replace_state);
}

if (import.meta.hot) {
	import.meta.hot.on('vite:beforeUpdate', () => {
		if (current.error) location.reload();
	});
}

/** @typedef {(typeof PRELOAD_PRIORITIES)['hover'] | (typeof PRELOAD_PRIORITIES)['tap']} PreloadDataPriority */

/**
 * The anchor element whose href is being preloaded. It is reset after navigation
 * or changes when a different anchor element is being preloaded.
 * @type {{ element: Element | SVGAElement | undefined; href: string | SVGAnimatedString | undefined }}
 */
let current_a = { element: undefined, href: undefined };

function setup_preload() {
	/** @type {NodeJS.Timeout} */
	let mousemove_timeout;
	/** @type {HTMLAnchorElement | SVGAElement | undefined} */
	let hovered_a;
	/** @type {PreloadDataPriority} */
	let current_priority;

	function clear_hover_preload() {
		clearTimeout(mousemove_timeout);
		hovered_a?.removeEventListener('mousemove', start_hover_preload);
		hovered_a?.removeEventListener('mouseleave', clear_hover_preload);
		hovered_a = undefined;
	}

	function start_hover_preload() {
		clearTimeout(mousemove_timeout);
		mousemove_timeout = setTimeout(() => {
			if (!hovered_a) return;
			void preload(hovered_a, PRELOAD_PRIORITIES.hover);
		}, 20);
	}

	// Use mouseover initially instead of mousemove to avoid cluttering the event queue
	container.addEventListener('mouseover', (event) => {
		if (!(event.target instanceof Element)) return;

		const a = find_anchor(event.target, container);
		if (!a || a === hovered_a) return;

		clear_hover_preload();
		hovered_a = a;

		const options = get_router_options(a);

		if (
			options.preload_code < PRELOAD_PRIORITIES.hover &&
			options.preload_data < PRELOAD_PRIORITIES.hover
		) {
			// don't add event listeners if no preloading will happen
			return;
		}

		// Instead of just preloading right away, we start a mousemove listener to implement
		// "mouse comes to a rest" behavior. This avoid false positives when you just move
		// your mouse across the screen and happen to pass over a link.
		a.addEventListener('mousemove', start_hover_preload);
		a.addEventListener('mouseleave', clear_hover_preload, { once: true });

		start_hover_preload();
	});

	/** @param {Event} event */
	function tap(event) {
		if (event.defaultPrevented) return;

		const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
		if (!a) return;

		void preload(a, PRELOAD_PRIORITIES.tap);
	}

	container.addEventListener('mousedown', tap);
	container.addEventListener('touchstart', tap, { passive: true });

	const observer = new IntersectionObserver(
		(entries) => {
			for (const entry of entries) {
				if (entry.isIntersecting) {
					void _preload_code(new URL(/** @type {HTMLAnchorElement} */ (entry.target).href));
					observer.unobserve(entry.target);
				}
			}
		},
		{ threshold: 0 }
	);

	/**
	 * @param {HTMLAnchorElement | SVGAElement} a
	 * @param {PreloadDataPriority} priority
	 */
	async function preload(a, priority) {
		// we don't want to preload data again if the user has already hovered/tapped
		const interacted =
			a === current_a.element && a?.href === current_a.href && priority >= current_priority;
		if (!a || interacted) return;

		const { url, external, download } = get_link_info(a, base, app.hash);
		if (external || download) return;

		const options = get_router_options(a);

		// we don't want to preload data for a page we're already on
		const same_url = url && get_page_key(current.url) === get_page_key(url);
		if (options.reload || same_url) return;

		if (priority <= options.preload_data) {
			current_a = { element: a, href: a.href };
			// we don't want to preload data again on tap if we've already preloaded it on hover
			current_priority = PRELOAD_PRIORITIES.tap;

			const intent = await get_navigation_intent(url, false);
			if (!intent) return;

			if (DEV) {
				void _preload_data(intent).catch((error) => {
					w.preload_data_failed({ path: intent.url.pathname, message: error.message });
				});
			} else {
				void _preload_data(intent).catch(noop);
			}
		} else if (priority <= options.preload_code) {
			current_a = { element: a, href: a.href };
			current_priority = priority;
			void _preload_code(/** @type {URL} */ (url));
		}
	}

	function after_navigate() {
		observer.disconnect();

		for (const a of container.querySelectorAll('a')) {
			const { url, external, download } = get_link_info(a, base, app.hash);
			if (external || download) continue;

			const options = get_router_options(a);
			if (options.reload) continue;

			if (options.preload_code === PRELOAD_PRIORITIES.viewport) {
				observer.observe(a);
			}

			if (options.preload_code === PRELOAD_PRIORITIES.eager) {
				void _preload_code(/** @type {URL} */ (url));
			}
		}
	}

	after_navigate_callbacks.add(after_navigate);
	after_navigate();
}

/**
 * @param {unknown} error
 * @param {NavigationEvent} event
 * @returns {Promise<App.Error>}
 */
export async function handle_error(error, event) {
	if (error instanceof HandledHttpError) {
		return error.body;
	}

	/** @type {import('@sveltejs/kit/hooks').ClientCaughtError} */
	let caught;

	if (error instanceof HttpError) {
		caught = { kind: 'app', error: error.body };
	} else if (error instanceof SvelteKitError) {
		caught = { kind: 'framework', error: { status: error.status, message: error.text } };
	} else {
		caught = { kind: 'unknown', error };
	}

	if (DEV && caught.kind !== 'app') {
		errored = true;
		w.hmr_reload_after_error();
	}

	const fallback =
		caught.kind === 'unknown' ? { status: 500, message: 'Internal Error' } : caught.error;

	const input = { ...caught, event };
	if (DEV) add_deprecated_handle_error_properties(input, fallback);

	const app_error = await app.hooks.handleError(input);

	// the hook returns only the properties it wants to override; anything it omits
	// (including by returning nothing at all) is inherited from the caught error
	return { ...fallback, ...app_error };
}

/**
 * @template {Function} T
 * @param {Set<T>} callbacks
 * @param {T} callback
 */
function add_navigation_callback(callbacks, callback) {
	onMount(() => {
		callbacks.add(callback);

		return () => {
			callbacks.delete(callback);
		};
	});
}

/**
 * A lifecycle function that runs the supplied `callback` when the current component mounts, and also whenever we navigate to a URL.
 *
 * `afterNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
 * @param {(navigation: AfterNavigate) => void} callback
 * @returns {void}
 */
export function afterNavigate(callback) {
	add_navigation_callback(after_navigate_callbacks, callback);
}

/**
 * A navigation interceptor that triggers before we navigate to a URL, whether by clicking a link, calling `goto(...)`, or using the browser back/forward controls.
 *
 * Calling `cancel()` will prevent the navigation from completing. If `navigation.type === 'leave'` — meaning the user is navigating away from the app (or closing the tab) — calling `cancel` will trigger the native browser unload confirmation dialog. In this case, the navigation may or may not be cancelled depending on the user's response.
 *
 * When a navigation isn't to a SvelteKit-owned route (and therefore controlled by SvelteKit's client-side router), `navigation.to.route.id` will be `null`.
 *
 * If the navigation will (if not cancelled) cause the document to unload — in other words `'leave'` navigations and `'link'` navigations where `navigation.to.route === null` — `navigation.willUnload` is `true`.
 *
 * `beforeNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
 * @param {(navigation: BeforeNavigate) => void} callback
 * @returns {void}
 */
export function beforeNavigate(callback) {
	add_navigation_callback(before_navigate_callbacks, callback);
}

/**
 * A lifecycle function that runs the supplied `callback` immediately before we navigate to a new URL except during full-page navigations.
 *
 * If you return a `Promise`, SvelteKit will wait for it to resolve before completing the navigation. This allows you to — for example — use `document.startViewTransition`. Avoid promises that are slow to resolve, since navigation will appear stalled to the user.
 *
 * If a function (or a `Promise` that resolves to a function) is returned from the callback, it will be called once the DOM has updated.
 *
 * `onNavigate` must be called during a component initialization. It remains active as long as the component is mounted.
 * @param {(navigation: OnNavigate) => import('types').MaybePromise<(() => void) | void>} callback
 * @returns {void}
 */
export function onNavigate(callback) {
	add_navigation_callback(on_navigate_callbacks, callback);
}

/**
 * If called when the page is being updated following a navigation (in `onMount` or `afterNavigate` or an action, for example), this disables SvelteKit's built-in scroll handling.
 * This is generally discouraged, since it breaks user expectations.
 * @returns {void}
 */
export function disableScrollHandling() {
	if (DEV && started && !updating) {
		e.scroll_handling_outside_navigation();
	}

	if (updating || !started) {
		disable_scroll_handling();
	}
}

let warned_on_invalidate_all = false;
let warned_on_replace_state = false;
let warned_on_push_state = false;
let warned_on_replace_state_function = false;

/**
 * @param {string | URL} url
 * @param {'goto' | 'pushState' | 'replaceState'} caller
 */
async function resolve_intent(url, caller) {
	const resolved = new URL(resolve_url(url));

	if (resolved.origin !== origin) {
		e.navigation_external_url({ caller, url: String(url) });
	}

	const intent = await get_navigation_intent(resolved, false);

	if (!intent) {
		e.navigation_route_missing({ caller, url: String(url) });
	}

	return intent;
}

/**
 * Allows you to navigate programmatically to a given route, with control over details such as whether scroll and focus are reset
 * (as they would be with a regular navigation) or preserved.
 *
 * Returns a Promise that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) or the state change has been applied.
 *
 * `goto` is intended for navigations to routes that belong to the app, and will reject if a route cannot be resolved.
 * For external URLs, use `window.location = url` to perform a full-page navigation instead of calling `goto(url)`.
 *
 * @param {string | URL} url Where to navigate to. Note that if you've set [`config.paths.base`](https://svelte.dev/docs/kit/configuration#paths) and the URL is root-relative, you need to prepend the base path if you want to navigate within the app.
 * @param {GotoOptions} [opts] Options related to the navigation
 * @returns {Promise<void>}
 */
export async function goto(url, opts = {}) {
	if (DEV) {
		if ('replaceState' in opts && !warned_on_replace_state) {
			warned_on_replace_state = true;
			w.goto_replace_state_deprecated({ value: String(opts.replaceState) });
		}

		if ('noScroll' in opts || 'keepFocus' in opts) {
			e.goto_options_removed();
		}
	}

	const replace = opts.replace ?? opts.replaceState ?? false;

	const intent = await resolve_intent(url, 'goto');

	if (opts.shallow) {
		return update_state(
			intent,
			opts.state ?? {},
			{
				replace,
				persist_state: opts.persistState ?? false,
				reset: opts.reset ?? false
			},
			'goto'
		);
	}

	if (DEV && 'invalidateAll' in opts && !warned_on_invalidate_all) {
		warned_on_invalidate_all = true;
		w.goto_invalidate_all_deprecated({ value: String(opts.invalidateAll) });
	}

	return _goto(
		intent.url,
		{ ...opts, replace, refreshAll: opts.refreshAll ?? opts.invalidateAll },
		0,
		{},
		intent
	);
}

/**
 * Causes any `load` functions belonging to the currently active page to re-run if they depend on the `url` in question, via `fetch` or `depends`. Returns a `Promise` that resolves when the page is subsequently updated.
 *
 * If the argument is given as a `string` or `URL`, it must resolve to the same URL that was passed to `fetch` or `depends` (including query parameters).
 * To create a custom identifier, use a string beginning with `[a-z]+:` (e.g. `custom:state`) — this is a valid URL.
 *
 * The `function` argument can be used define a custom predicate. It receives the full `URL` and causes `load` to rerun if `true` is returned.
 * This can be useful if you want to invalidate based on a pattern instead of a exact match.
 *
 * ```ts
 * // Example: Match '/path' regardless of the query parameters
 * import { invalidate } from '$app/navigation';
 *
 * invalidate((url) => url.pathname === '/path');
 * ```
 * @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
 * @param {boolean} [keepState] If `true`, the current `page.state` will be preserved. Otherwise, it will be reset to an empty object. `false` by default.
 * @returns {Promise<void>}
 */
export function invalidate(resource, keepState = false) {
	push_invalidated(resource);
	return _invalidate(!keepState);
}

/**
 * @param {string | URL | ((url: URL) => boolean)} resource The invalidated URL
 */
function push_invalidated(resource) {
	if (typeof resource === 'function') {
		invalidated.push(resource);
	} else {
		const { href } = new URL(resource, location.href);
		invalidated.push((url) => url.href === href);
	}
}

/**
 * Causes all `load` and `query` functions belonging to the currently active page to re-run. Returns a `Promise` that resolves when the page is subsequently updated.
 *
 * Note that this resets `page.state` to an empty object. If you want to preserve `page.state` (for example when using [shallow routing](https://svelte.dev/docs/kit/shallow-routing)), use `refreshAll` instead.
 *
 * @deprecated Use [`refreshAll`](https://svelte.dev/docs/kit/$app-navigation#refreshAll) instead. Unlike `invalidateAll`, `refreshAll` does not reset `page.state`.
 * @returns {Promise<void>}
 */
export function invalidateAll() {
	force_invalidation = true;
	return _invalidate();
}

/**
 * Causes all currently active remote functions to refresh, and all `load` functions belonging to the currently active page to re-run.
 * Returns a `Promise` that resolves when the page is subsequently updated.
 * @returns {Promise<void>}
 */
export function refreshAll() {
	force_invalidation = true;
	return _invalidate(false);
}

/**
 * Programmatically preloads the given page, which means
 *  1. ensuring that the code for the page is loaded, and
 *  2. calling the page's load function with the appropriate options.
 *
 * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `<a>` element with `data-sveltekit-preload-data`.
 * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous.
 * Returns a Promise that resolves with the result of running the new route's `load` functions once the preload is complete.
 *
 * @param {string} href Page to preload
 * @returns {Promise<({ type: 'loaded'; data: Record<string, any> } | { type: 'redirect'; location: string } | { type: 'error'; error: App.Error }) & { status: number; }>}
 */
export async function preloadData(href) {
	const url = resolve_url(href);
	const intent = await get_navigation_intent(url, false);

	if (!intent) {
		e.preload_url_outside_app({ url: url.href });
	}

	/** @type {Awaited<ReturnType<typeof _preload_data>>} */
	let result;

	try {
		result = await _preload_data(intent);
	} catch (error) {
		// `load_route` throws the handled error (an `App.Error` with a `status`)
		// when a preload fails, so surface it in the documented `{ type: 'error' }` shape
		const handled = /** @type {App.Error & { status?: number }} */ (error);
		return {
			type: 'error',
			status: handled?.status ?? 500,
			error: handled
		};
	}

	if (result.type === 'redirect') {
		return {
			type: result.type,
			status: result.status,
			location: result.location
		};
	}

	const { status, data } = result.props.page ?? page;

	return { type: result.type, status, data };
}

/**
 * Programmatically imports the code for routes that haven't yet been fetched.
 * Typically, you might call this to speed up subsequent navigation.
 *
 * Takes a route ID such as `/about` or `/blog/[slug]`. Unlike pathnames, route IDs
 * are never prefixed with the app's [base path](https://svelte.dev/docs/kit/configuration#paths).
 * If you have a pathname rather than a route ID, you can convert it with
 * [`match`](https://svelte.dev/docs/kit/$app-paths#match) from `$app/paths`:
 *
 * ```js
 * import { match } from '$app/paths';
 * import { preloadCode } from '$app/navigation';
 *
 * const matched = await match('/blog/hello-world');
 * if (matched) await preloadCode(matched.id);
 * ```
 *
 * Unlike `preloadData`, this won't call `load` functions.
 * Returns a Promise that resolves when the modules have been imported.
 *
 * @param {import('$app/types').RouteId} id
 * @returns {Promise<void>}
 */
export async function preloadCode(id) {
	if (DEV && id[0] !== '/') {
		e.preload_invalid_route_id({ id });
	}

	const route = __SVELTEKIT_CLIENT_ROUTING__
		? routes.find((r) => r.id === id)
		: (route_id_cache.get(id) ?? (await load_route_by_id(id)));

	if (route === ENDPOINT_ONLY) {
		if (DEV) {
			w.preload_code_endpoint_only({ id });
		}

		return;
	}

	if (!route) {
		if (DEV) {
			// warn rather than throw, since under client routing an endpoint-only route id is
			// indistinguishable from a typo — the client manifest only contains routes with a `+page`
			// the most common migration mistake is passing a pathname, which used to work
			const candidates = [id];
			if (base && id.startsWith(base)) candidates.push(id.slice(base.length) || '/');

			if (
				__SVELTEKIT_CLIENT_ROUTING__ &&
				candidates.some((path) => routes.some((r) => r.exec(path)))
			) {
				w.preload_route_is_pathname({ id });
			} else {
				w.preload_route_missing({ id });
			}
		}

		return;
	}

	await load_route_nodes(route);
}

/**
 * Programmatically create a new history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
 *
 * @deprecated Use `goto(url, { state, shallow: true })` instead.
 * @param {string | URL} url
 * @param {App.PageState} state
 * @returns {Promise<void>}
 */
export async function pushState(url, state) {
	if (DEV && !warned_on_push_state) {
		warned_on_push_state = true;
		w.push_state_deprecated();
	}

	const intent = await resolve_intent(url, 'pushState');

	await update_state(
		intent,
		state,
		{ replace: false, persist_state: false, reset: false },
		'pushState'
	);
}

/**
 * Programmatically replace the current history entry with the given `page.state`. Used for [shallow routing](https://svelte.dev/docs/kit/shallow-routing).
 *
 * @deprecated Use `goto(url, { state, shallow: true, replace: true })` instead.
 * @param {string | URL} url
 * @param {App.PageState} state
 * @returns {Promise<void>}
 */
export async function replaceState(url, state) {
	if (DEV && !warned_on_replace_state_function) {
		warned_on_replace_state_function = true;
		w.replace_state_deprecated();
	}

	const intent = await resolve_intent(url, 'replaceState');

	await update_state(
		intent,
		state,
		{ replace: true, persist_state: false, reset: false },
		'replaceState'
	);
}

/**
 * @param {NavigationIntent} intent
 * @param {App.PageState} state
 * @param {{ replace: boolean; persist_state: boolean; reset: boolean; }} options
 * @param {'goto' | 'pushState' | 'replaceState'} caller
 */
async function update_state(intent, state, { replace, persist_state, reset }, caller) {
	const url = intent.url;
	const previous_snapshot_registrations = current_registrations();

	if (DEV && !started) {
		e.navigation_before_start({ caller });
	}

	const nav =
		// For backwards compatibility we don't trigger navigation hooks etc for push/replaceState
		caller === 'goto' ? _before_navigate({ url, type: 'goto', intent, shallow: true }) : undefined;

	if (!nav && caller === 'goto') return;

	const nav_token = {};

	if (nav) {
		navigation_token = invalidation_token = nav_token;
		is_navigating = true;
		set_navigation(nav.navigation);
		updating = true;
	}

	if (replace) {
		delete_navigation_snapshot(current_history_index);
	} else {
		capture_scroll(current_history_index);
		capture_navigation_snapshot(current_history_index);
	}
	if (reset) current_reset_index += 1;

	// as above, serialize-then-parse to prevent bugs
	const serialized_state = stringify(state);
	state = parse(serialized_state);

	const entry = {
		[HISTORY_METADATA_KEY]: /** @satisfies {HistoryMetadata} */ ({
			historyIndex: (current_history_index += replace ? 0 : 1),
			navigationIndex: current_navigation_index,
			pageUrl: page.url.href,
			state: serialized_state,
			persistState: persist_state,
			resetIndex: current_reset_index
		})
	};

	const fn = replace ? history.replaceState : history.pushState;
	fn.call(history, entry, '', url);
	set_history_options(current_history_index, entry[HISTORY_METADATA_KEY]);

	if (!replace) {
		has_navigated = true;
		clear_onward_history(current_history_index, current_navigation_index);
	}

	if (nav) {
		await run_on_navigate_callbacks(/** @type {OnNavigate} */ (nav.navigation));
	}

	blur_active_element(reset);

	update_page({
		state,
		shallow: {
			params: intent?.params ?? null,
			route: intent ? { id: intent.route.id } : null,
			url
		}
	});

	if (nav) {
		const finished = await finish_navigation(nav, nav_token, url, null, reset, settled());
		if (!finished) return;
	}

	restore_navigation_snapshot(current_history_index, previous_snapshot_registrations);

	if (nav) {
		set_navigation(null);
		updating = false;
	}
}

/**
 * Updates the `form` property of the current page with the given data and updates `page.status`.
 * In case of an error, it renders the nearest error page. In case of a redirect, it navigates to
 * the redirect location.
 * @template {Record<string, unknown> | undefined} Success
 * @template {Record<string, unknown> | undefined} Failure
 * @param {ActionResult<Success, Failure>} result
 * @returns {Promise<void>}
 */
export async function applyAction(result) {
	if (result.type === 'redirect') {
		await _goto(result.location, { refreshAll: true });
		return;
	}

	if (result.type === 'error') {
		await set_nearest_error_page(result.error);
	} else {
		update_page({
			form: result.data,
			status: result.status
		});

		/** @type {Record<string, any>} */
		// this brings Svelte's view of the world in line with SvelteKit's
		// after use:enhance reset the form....
		props.form = null;

		// ...so that setting the `form` prop takes effect and isn't ignored
		await tick();
		props.form = result.data;

		if (result.type === 'success') {
			reset_focus(/** @type {URL} */ (page.url));
		}
	}
}

/**
 * Whether a submission should update the current page in place rather than navigating — either
 * because it lands where we already are, or because the result carries no location at all
 * (hand-constructed results, which have always applied in place).
 * @param {string | undefined} value
 */
export function is_current_location(value) {
	if (value === undefined) return true;

	const destination = resolve_url(value);
	const current = new URL(location.href);
	if (destination.origin !== current.origin || destination.pathname !== current.pathname)
		return false;

	const keys = new Set([...destination.searchParams.keys(), ...current.searchParams.keys()]);
	for (const key of keys) {
		const destination_values = destination.searchParams.getAll(key).sort();
		const current_values = current.searchParams.getAll(key).sort();

		if (
			destination_values.length !== current_values.length ||
			destination_values.some((value, i) => value !== current_values[i])
		) {
			return false;
		}
	}

	return true;
}

/**
 * Navigate to where a form submission should land, rendering that page with the given result
 * — what the browser would do for a native submission.
 * @param {string} location
 * @param {ActionResult} result
 * @param {boolean} refresh_all
 * @returns {Promise<void>}
 */
export function apply_action_navigation(location, result, refresh_all) {
	return _goto(location, {
		type: 'form',
		refreshAll: refresh_all,
		action_result: result
	});
}

/**
 * @param {App.Error} error
 */
export async function set_nearest_error_page(error) {
	const url = new URL(location.href);

	const { branch, route } = current;
	if (!route) return;

	const error_branch = await load_nearest_error_page(current.branch.length, branch, route.errors);
	if (error_branch) {
		const navigation_result = await get_navigation_result_from_branch({
			url,
			params: current.params,
			branch: error_branch,
			error,
			errors: route.errors,
			route
		});

		current = { ...navigation_result.state, nav: current.nav };

		apply_navigation_result(navigation_result);

		void tick().then(() => reset_focus(current.url));
	}
}

function _start_router() {
	history.scrollRestoration = 'manual';

	addEventListener('beforeunload', (e) => {
		let should_block = false;

		persist_state();

		if (!is_navigating) {
			const nav = create_navigation(current, undefined, null, 'leave');

			// If we're navigating, beforeNavigate was already called. If we end up in here during navigation,
			// it's due to an external or full-page-reload link, for which we don't want to call the hook again.
			/** @type {BeforeNavigate} */
			const navigation = {
				...nav.navigation,
				cancel: () => {
					should_block = true;
					nav.reject(new Error('navigation cancelled'));
				}
			};

			before_navigate_callbacks.forEach((fn) => fn(navigation));
		}

		if (should_block) {
			e.preventDefault();
			e.returnValue = '';
		}
	});
	addEventListener('visibilitychange', () => {
		if (document.visibilityState === 'hidden') {
			persist_state();
		} else {
			// the tab just became visible — a good time to check for a new deployment
			void updated.check();
		}
	});

	addEventListener('focus', () => {
		void updated.check();
	});

	// @ts-expect-error this isn't supported everywhere yet
	if (!navigator.connection?.saveData) {
		setup_preload();
	}

	/** @param {MouseEvent} event */
	container.addEventListener('click', async (event) => {
		// Adapted from https://github.com/visionmedia/page.js
		// MIT license https://github.com/visionmedia/page.js#license
		if (event.button) return;
		if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
		if (event.defaultPrevented) return;

		const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
		if (!a) return;

		const { url, external, target, download } = get_link_info(a, base, app.hash);
		if (!url) return;

		// bail out before `beforeNavigate` if link opens in a different tab
		if (target === '_parent' || target === '_top') {
			if (window.parent !== window) return;
		} else if (target && target !== '_self') {
			return;
		}

		const options = get_router_options(a);
		const is_svg_a_element = a instanceof SVGAElement;

		// Ignore URL protocols that differ to the current one and are not http(s) (e.g. `mailto:`, `tel:`, `myapp:`, etc.)
		// This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external
		// navigation, but it's not clear how to handle that case and it's not likely to come up in practice.
		// See:
		// - https://github.com/sveltejs/kit/issues/4045
		// - https://github.com/sveltejs/kit/issues/5725
		// - https://github.com/sveltejs/kit/issues/6496
		if (
			!is_svg_a_element &&
			url.protocol !== location.protocol &&
			!(url.protocol === 'https:' || url.protocol === 'http:')
		)
			return;

		if (download) return;

		const [nonhash, hash] = (app.hash ? url.hash.replace(/^#/, '') : url.href).split('#');
		const same_pathname = nonhash === strip_hash(location);

		// Ignore the following but fire beforeNavigate
		if (external || (options.reload && (!same_pathname || !hash))) {
			if (_before_navigate({ url, type: 'link', event })) {
				// set `navigating` to `true` to prevent `beforeNavigate` callbacks
				// being called when the page unloads
				is_navigating = true;
			} else {
				event.preventDefault();
			}

			return;
		}

		// Check if new url only differs by hash and use the browser default behavior in that case
		// This will ensure the `hashchange` event is fired
		// Removing the hash does a full page navigation in the browser, so make sure a hash is present
		if (hash !== undefined && same_pathname) {
			// If we are trying to navigate to the same hash, we should only
			// attempt to scroll to that element and avoid any history changes.
			// Otherwise a fragment navigation creates a new entry with a null history state,
			// which browsers handle differently: https://github.com/whatwg/html/issues/6213
			const [, current_hash] = current.url.href.split('#');
			if (current_hash === hash) {
				event.preventDefault();

				// We're already on /# and click on a link that goes to /#, or we're on
				// /#top and click on a link that goes to /#top. In those cases just go to
				// the top of the page, and avoid a history change.
				if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) {
					scrollTo({ top: 0 });
				} else {
					const element = a.ownerDocument.getElementById(decodeURIComponent(hash));
					if (element) {
						element.scrollIntoView();
						element.focus();
					}
				}

				return;
			}
			// set this flag to distinguish between navigations triggered by
			// clicking a hash link and those triggered by popstate. We gotta retrieve
			// history metadata here because the hashchange event will occur after history.state was updated
			hash_navigating = {
				.../** @type {HistoryMetadata} */ (get_history_metadata()),
				resetIndex: current_reset_index + (options.reset ? 1 : 0)
			};

			capture_scroll(current_history_index);

			update_url(url);

			if (!options.replace_state) return;

			// hashchange event shouldn't occur if the router is replacing state.
			hash_navigating = null;
		}

		event.preventDefault();

		// allow the browser to repaint before navigating —
		// this prevents INP scores being penalised
		await new Promise((fulfil) => {
			requestAnimationFrame(() => {
				setTimeout(fulfil, 0);
			});

			setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded
		});

		const changed = url.href !== location.href;

		await _goto(url, {
			type: 'link',
			reset: options.reset,
			replace: options.replace_state ?? !changed,
			refreshAll: !changed,
			event
		});
	});

	container.addEventListener('submit', (event) => {
		if (event.defaultPrevented) return;

		const form = /** @type {HTMLFormElement} */ (
			HTMLFormElement.prototype.cloneNode.call(event.target)
		);

		const submitter = /** @type {HTMLButtonElement | HTMLInputElement | null} */ (event.submitter);

		const target = submitter?.formTarget || form.target;

		if (target === '_blank') return;

		const method = submitter?.formMethod || form.method;

		if (method !== 'get') return;

		// It is impossible to use form actions with hash router, so we just ignore handling them here
		const url = new URL(
			(submitter?.hasAttribute('formaction') && submitter?.formAction) || form.action
		);

		if (is_external_url(url, base, false)) return;

		const event_form = /** @type {HTMLFormElement} */ (event.target);

		const options = get_router_options(event_form);
		if (options.reload) return;

		event.preventDefault();
		event.stopPropagation();

		const data = new FormData(event_form, submitter);

		// @ts-expect-error `URLSearchParams(fd)` is kosher, but typescript doesn't know that
		url.search = new URLSearchParams(data).toString();

		void navigate({
			type: 'form',
			url,
			reset: options.reset,
			replace_state: options.replace_state ?? url.href === location.href,
			event
		});
	});

	addEventListener('popstate', async (event) => {
		const history_metadata = get_history_metadata(event.state);

		if (history_metadata?.historyIndex) {
			const history_index = history_metadata.historyIndex;
			const source_info = history_info[current_history_index];

			// if a popstate-driven navigation is cancelled, we need to counteract it
			// with history.go, which means we end up back here, hence this check
			if (history_index === current_history_index) return;

			navigation_token = invalidation_token = {};

			const delta = history_index - current_history_index;
			const reset_index = history_metadata.resetIndex;
			const reset = reset_index !== (source_info?.resetIndex ?? current_reset_index);
			const scroll = history_info[history_index]?.scroll;
			const state = parse(history_metadata.state);
			const url = new URL(history_metadata.pageUrl ?? location.href);
			const navigation_index = history_metadata.navigationIndex;
			const is_hash_change =
				current.url && (location.href + current.url.href).includes('#') // check if even has a hash
					? strip_hash(location) === strip_hash(current.url)
					: false;
			const shallow =
				navigation_index === current_navigation_index &&
				((has_navigated &&
					(history_metadata.pageUrl === undefined || history_metadata.pageUrl === location.href)) ||
					is_hash_change);
			const shallow_url = history_metadata.pageUrl ? new URL(location.href) : null;

			// the browser has already traversed, so record the source entry and move the
			// indices before anything async: a navigation that starts while this one
			// resolves must build on the entry we are actually on
			const previous_history_index = current_history_index;
			const previous_navigation_index = current_navigation_index;
			const previous_reset_index = current_reset_index;
			capture_scroll(previous_history_index);
			if (!shallow) capture_snapshot(previous_navigation_index);
			capture_navigation_snapshot(previous_history_index);
			current_history_index = history_index;
			current_navigation_index = navigation_index;
			current_reset_index = reset_index;

			const token = navigation_token;
			const shallow_intent = shallow_url
				? await get_navigation_intent(shallow_url, false)
				: undefined;
			if (navigation_token !== token) return;
			const shallow_target = shallow_url
				? {
						params: shallow_intent?.params ?? null,
						route: shallow_intent ? { id: shallow_intent.route.id } : null,
						url: shallow_url
					}
				: null;

			if (shallow) {
				// We don't need to navigate, we just need to update scroll and/or state.
				// This happens with hash links and `pushState`/`replaceState`. The
				// exception is if we haven't navigated yet, since we could have
				// got here after a modal navigation then a reload

				blur_active_element(reset);

				update_page({
					state,
					shallow: shallow_target
				});

				update_url(url);

				if (reset && scroll) scrollTo(scroll.x, scroll.y);
				restore_navigation_snapshot(current_history_index, current_registrations());
				// the token above aborted any in-flight navigation, and nothing else will clear it
				is_navigating = false;
				set_navigation(null);
				return;
			}

			await navigate({
				type: 'popstate',
				url,
				reset,
				popped: {
					state,
					scroll,
					delta,
					shallow: shallow_target
				},
				block: () => {
					current_history_index = previous_history_index;
					current_navigation_index = previous_navigation_index;
					current_reset_index = previous_reset_index;
					history.go(-delta);
				},
				nav_token: token,
				event
			});
		} else {
			// since popstate event is also emitted when an anchor referencing the same
			// document is clicked, we have to check that the router isn't already handling
			// the navigation. otherwise we would be updating the page store twice.
			if (!hash_navigating) {
				const url = new URL(location.href);
				update_url(url);

				// if the user edits the hash via the browser URL bar, trigger a full-page
				// reload to align with pathname router behavior
				if (app.hash) {
					location.reload();
				}
			}
		}
	});

	addEventListener('hashchange', () => {
		// if the hashchange happened as a result of clicking on a link,
		// we need to update history, otherwise we have to leave it alone
		if (hash_navigating) {
			const history_metadata = hash_navigating;
			hash_navigating = null;
			current_reset_index = history_metadata.resetIndex;
			history.replaceState(
				{
					...history.state,
					[HISTORY_METADATA_KEY]: {
						...history_metadata,
						historyIndex: ++current_history_index,
						navigationIndex: current_navigation_index
					}
				},
				'',
				location.href
			);
			set_history_options(current_history_index, history_metadata);
		}
	});

	// fix link[rel=icon], because browsers will occasionally try to load relative
	// URLs after a pushState/replaceState, resulting in a 404 — see
	// https://github.com/sveltejs/kit/issues/3748#issuecomment-1125980897
	for (const link of document.querySelectorAll('link')) {
		if (ICON_REL_ATTRIBUTES.has(link.rel)) {
			link.href = link.href; // eslint-disable-line
		}
	}

	addEventListener('pageshow', (event) => {
		// If the user navigates to another site and then uses the back button and
		// bfcache hits, we need to set navigating to null, the site doesn't know
		// the navigation away from it was successful.
		// Info about bfcache here: https://web.dev/bfcache
		if (event.persisted) {
			set_navigation(null);

			// pages restored from bfcache don't re-run the `start` script
			// so we need to restore the scroll position manually here
			const scroll = history_info[current_history_index]?.scroll;
			if (scroll) {
				scrollTo(scroll.x, scroll.y);
			}
		}
	});

	/**
	 * @param {URL} url
	 */
	function update_url(url) {
		current.url = url;
		update_page({ url });
	}
}

/**
 * @param {HTMLElement} target
 * @param {import('./types.js').HydrateOptions} opts
 * @returns {Promise<void>}
 */
async function _hydrate(
	target,
	{ status, error, node_ids, params, route, server_route, data: server_data_nodes, form }
) {
	hydrated = true;

	const url = new URL(location.href);

	/** @type {import('types').CSRRoute | undefined} */
	let parsed_route;

	if (__SVELTEKIT_CLIENT_ROUTING__) {
		if (!__SVELTEKIT_EMBEDDED__) {
			// See https://github.com/sveltejs/kit/pull/4935#issuecomment-1328093358 for one motivation
			// of determining the params on the client side.
			({ params = {}, route = { id: null } } = (await get_navigation_intent(url, false)) || {});
		}

		parsed_route = routes.find(({ id }) => id === route.id);
	} else {
		// undefined in case of 404
		if (server_route) {
			parsed_route = route = parse_and_cache_server_route(server_route);
		} else {
			route = { id: null };
			params = {};
		}
	}

	/** @type {import('./types.js').NavigationFinished | undefined} */
	let result;
	let should_hydrate = true;

	try {
		const branch_promises = node_ids.map(async (n, i) => {
			const server_data_node = server_data_nodes[i];
			// Type isn't completely accurate, we still need to deserialize uses
			if (server_data_node?.uses) {
				server_data_node.uses = deserialize_uses(server_data_node.uses);
			}

			return load_node({
				loader: app.nodes[n],
				url,
				params,
				route,
				parent: async () => {
					const data = {};
					for (let j = 0; j < i; j += 1) {
						Object.assign(data, (await branch_promises[j]).data);
					}
					return data;
				},
				server_data_node: create_data_node(server_data_node)
			});
		});

		/** @type {Array<import('./types.js').BranchNode | undefined>} */
		const branch = await Promise.all(branch_promises);

		// server-side will have compacted the branch, reinstate empty slots
		// so that error boundaries can be lined up correctly
		if (parsed_route) {
			const layouts = parsed_route.layouts;
			for (let i = 0; i < layouts.length; i++) {
				if (!layouts[i]) {
					branch.splice(i, 0, undefined);
				}
			}
		}

		result = await get_navigation_result_from_branch({
			url,
			params,
			branch,
			status,
			error,
			errors: parsed_route?.errors, // TODO load earlier?
			form,
			route: parsed_route ?? null
		});
	} catch (error) {
		if (error instanceof Redirect) {
			// this is a real edge case — `load` would need to return
			// a redirect but only in the browser
			return await native_navigation(new URL(error.location, location.href));
		}

		const handled_error = await handle_error(error, { url, params, route });

		result = await load_root_error_page({
			error: handled_error,
			url,
			route
		});

		target.textContent = '';
		should_hydrate = false;
	}

	// Exit early when we encounter a redirect while loading the root error page.
	// In this case, `initialize` will be called later on
	if (!result) return;

	if (result.props.page) {
		const history_metadata = get_history_metadata();
		result.props.page.state = history_metadata?.persistState ? parse(history_metadata.state) : {};
	}

	await initialize(result, target, should_hydrate);
}

/**
 * @param {URL} url
 * @param {boolean[]} invalid
 * @returns {Promise<import('types').ServerNodesResponse | import('types').ServerRedirectNode>}
 */
async function load_data(url, invalid) {
	for (const key of url.searchParams.keys()) {
		if (key.startsWith('x-sveltekit-')) {
			e.reserved_query_parameter({ key });
		}
	}

	const data_url = new URL(url);
	data_url.pathname = add_data_suffix(url.pathname);
	if (url.pathname.endsWith('/')) {
		data_url.searchParams.append(TRAILING_SLASH_PARAM, '1');
	}
	data_url.searchParams.append(INVALIDATED_PARAM, invalid.map((i) => (i ? '1' : '0')).join(''));

	// use window.fetch directly to allow using a 3rd party-patched fetch implementation
	const fetcher = DEV ? dev_fetch : window.fetch;
	const res = await fetcher(data_url.href, {});

	// detect new deployments from the response header
	notify_version(res.headers.get('x-sveltekit-version'));

	if (!res.ok) {
		// if `__data.json` doesn't exist or the server has an internal error,
		// avoid parsing the HTML error page as a JSON
		/** @type {App.Error} */
		let error = { status: res.status, message: 'Internal Error' };

		if (res.headers.get('content-type')?.includes('application/json')) {
			error = { status: res.status, ...(await res.json()) };
		} else if (res.status === 404) {
			error.message = 'Not Found';
		}

		throw new HandledHttpError(error);
	}

	return new Promise((resolve, reject) => {
		process_stream(resolve, res).catch(reject);
	});

	// TODO edge case handling necessary? stream() read fails?
}

/**
 * @param {(value: ServerNodesResponse | ServerRedirectNode) => void} resolve
 * @param {Response} res
 * @returns {Promise<void>}
 */
async function process_stream(resolve, res) {
	const reader = /** @type {ReadableStream<Uint8Array>} */ (res.body).getReader();

	/**
	 * Map of deferred promises that will be resolved by a subsequent chunk of data
	 * @type {Map<string, import('types').Deferred>}
	 */
	const deferreds = new Map();

	/**
	 * @param {any} data
	 */
	function deserialize(data) {
		return devalue.unflatten(data, {
			...app.decoders,
			Promise: (id) => {
				return new Promise((fulfil, reject) => {
					deferreds.set(id, { fulfil, reject });
				});
			}
		});
	}

	for await (const node of read_ndjson(reader)) {
		if (node.type === 'redirect') {
			return resolve(node);
		}

		if (node.type === 'data') {
			// This is the first (and possibly only, if no pending promises) chunk
			node.nodes?.forEach((/** @type {any} */ node) => {
				if (node?.type === 'data') {
					node.uses = deserialize_uses(node.uses);
					node.data = deserialize(node.data);
				}
			});

			resolve(node);
		} else if (node.type === 'chunk') {
			// This is a subsequent chunk containing deferred data
			const { id, data, error } = node;
			const deferred = /** @type {import('types').Deferred} */ (deferreds.get(id));
			deferreds.delete(id);

			if (error) {
				deferred.reject(deserialize(error));
			} else {
				deferred.fulfil(deserialize(data));
			}
		}
	}
}

/**
 * @param {any} uses
 * @return {import('types').Uses}
 */
function deserialize_uses(uses) {
	return {
		dependencies: new Set(uses?.dependencies ?? []),
		params: new Set(uses?.params ?? []),
		parent: !!uses?.parent,
		route: !!uses?.route,
		url: !!uses?.url,
		search_params: new Set(uses?.search_params ?? [])
	};
}

/**
 * @template {NavigationType} T
 * @param {import('./types.js').NavigationState} current
 * @param {import('./types.js').NavigationIntent | undefined} intent
 * @param {URL | null} url
 * @param {T} type
 * @param {{ x: number, y: number } | null} [target_scroll] The scroll position for the target (for popstate navigations)
 * @param {boolean} [shallow]
 * @param {{ params: Record<string, string> | null; route: { id: string } | null; url: URL }} [target]
 */
function create_navigation(
	current,
	intent,
	url,
	type,
	target_scroll = null,
	shallow = false,
	target
) {
	/** @type {(value: any) => void} */
	let fulfil;

	/** @type {(error: any) => void} */
	let reject;

	const complete = new Promise((f, r) => {
		fulfil = f;
		reject = r;
	});

	// Handle any errors off-chain so that it doesn't show up as an unhandled rejection
	complete.catch(noop);

	/** @type {(Navigation | AfterNavigate) & { type: T }} */
	const navigation = /** @type {any} */ ({
		from: {
			params: current.params,
			route: { id: current.route?.id ?? null },
			url: current.url,
			scroll: scroll_state()
		},
		to: url && {
			params: target ? target.params : (intent?.params ?? null),
			route: { id: target ? (target.route?.id ?? null) : (intent?.route?.id ?? null) },
			url: target?.url ?? url,
			scroll: target_scroll
		},
		willUnload: !shallow && !intent,
		type,
		shallow,
		complete
	});

	return {
		navigation,
		// @ts-expect-error
		fulfil,
		// @ts-expect-error
		reject
	};
}

/**
 * @param {URL} url
 * @returns {URL}
 */
function decode_hash(url) {
	const new_url = new URL(url);
	// Safari, for some reason, does change # to %23, when entered through the address bar
	new_url.hash = decodeURIComponent(url.hash);
	return new_url;
}

if (DEV) {
	if (import.meta.hot) {
		import.meta.hot.on('vite:beforeUpdate', () => {
			if (errored) {
				location.reload();
			}
		});
	}
}

/**
 * @param {NavigationFinished} result
 */
function apply_navigation_result(result) {
	update_page(result.props.page);

	props.tree.data = result.props.tree.data;
	props.tree.child = result.props.tree.child;

	if ('form' in result.props) {
		props.form = result.props.form;
	}

	if ('error' in result.props) {
		props.error = result.props.error;
	}
}