import React, {
createContext,
startTransition,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { hydrateRoot } from "react-dom/client";
import { renderToReadableStream } from "react-dom/server.browser";
import { ReadableStream, TransformStream, WritableStream } from "web-streams-polyfill";
const PageContext = createContext(null);
const RouterContext = createContext(null);
const DEFAULT_RUNTIME_CONFIG = Object.freeze({});
const DEFAULT_NAVIGATION_STATE = Object.freeze({
pending: false,
progress: 0,
visible: false,
targetUrl: null,
navigationType: null,
});
const DEFAULT_SERVER_ROUTER = Object.freeze({
navigation: DEFAULT_NAVIGATION_STATE,
state: {},
});
if (!globalThis.__RUNTIME__) {
globalThis.__RUNTIME__ = {
now() {
return Date.now();
},
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, Number(ms)));
},
log(level, message) {
const fn = console?.[level] ?? console.log;
fn(String(message));
},
};
}
function runtimeBridge() {
return globalThis.__RUNTIME__;
}
function runtimeConfig() {
return globalThis.__RUNTIME_CONFIG__ ?? DEFAULT_RUNTIME_CONFIG;
}
function pageTitle(envelope) {
return envelope.meta?.title ?? runtimeConfig().title ?? "App";
}
function isFileInstance(value) {
return typeof File !== "undefined" && value instanceof File;
}
function isBlobInstance(value) {
return typeof Blob !== "undefined" && value instanceof Blob;
}
function isFileListInstance(value) {
return typeof FileList !== "undefined" && value instanceof FileList;
}
function isBinaryFormValue(value) {
return isFileInstance(value) || isBlobInstance(value);
}
if (!globalThis.ReadableStream) {
globalThis.ReadableStream = ReadableStream;
}
if (!globalThis.TransformStream) {
globalThis.TransformStream = TransformStream;
}
if (!globalThis.WritableStream) {
globalThis.WritableStream = WritableStream;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function serializePageEnvelope(value) {
return JSON.stringify(value)
.replaceAll("&", "\\u0026")
.replaceAll("<", "\\u003c")
.replaceAll(">", "\\u003e")
.replaceAll("\u2028", "\\u2028")
.replaceAll("\u2029", "\\u2029");
}
function normalizeHeadEntry(entry) {
if (!entry?.tag) {
return null;
}
if (entry.tag === "meta") {
if ((!entry.name && !entry.property) || entry.content == null) {
return null;
}
return {
tag: "meta",
name: entry.name ? String(entry.name) : undefined,
property: entry.property ? String(entry.property) : undefined,
content: String(entry.content),
};
}
if (entry.tag === "link") {
if (!entry.rel || !entry.href) {
return null;
}
return {
tag: "link",
rel: String(entry.rel),
href: String(entry.href),
};
}
return null;
}
function normalizeHeadEntries(entries = []) {
return entries.map(normalizeHeadEntry).filter(Boolean);
}
function pageEnvelopeCache() {
if (!globalThis.__HAVEN_PAGE_ENVELOPE_CACHE__) {
globalThis.__HAVEN_PAGE_ENVELOPE_CACHE__ = new Map();
}
return globalThis.__HAVEN_PAGE_ENVELOPE_CACHE__;
}
function inflightPrefetches() {
if (!globalThis.__HAVEN_PAGE_ENVELOPE_PREFETCHES__) {
globalThis.__HAVEN_PAGE_ENVELOPE_PREFETCHES__ = new Map();
}
return globalThis.__HAVEN_PAGE_ENVELOPE_PREFETCHES__;
}
function currentPageVersion(currentEnvelope) {
return currentEnvelope?.page?.version ?? runtimeConfig().version ?? "dev";
}
function absolutePageUrl(url) {
return new URL(url, window.location.href).href;
}
function envelopeCacheKey(url, version) {
return `${version}:${absolutePageUrl(url)}`;
}
function getCachedEnvelope(url, version) {
return pageEnvelopeCache().get(envelopeCacheKey(url, version)) ?? null;
}
function setCachedEnvelope(url, version, envelope) {
pageEnvelopeCache().set(envelopeCacheKey(url, version), envelope);
return envelope;
}
function renderHeadEntry(entry) {
if (entry.tag === "meta") {
const nameAttr = entry.name ? ` name="${escapeHtml(entry.name)}"` : "";
const propertyAttr = entry.property ? ` property="${escapeHtml(entry.property)}"` : "";
return `<meta data-haven-head="true"${nameAttr}${propertyAttr} content="${escapeHtml(entry.content)}">`;
}
if (entry.tag === "link") {
return `<link data-haven-head="true" rel="${escapeHtml(entry.rel)}" href="${escapeHtml(entry.href)}">`;
}
return "";
}
function renderManagedHeadTags(entries = []) {
return normalizeHeadEntries(entries)
.map(renderHeadEntry)
.filter(Boolean)
.join("\n ");
}
function applyLayouts(layouts, layoutInput, pageElement) {
if (!Array.isArray(layouts) || layouts.length === 0) {
return pageElement;
}
return layouts.reduceRight((children, Layout) => {
if (!Layout) {
return children;
}
return Layout({
...layoutInput,
children,
});
}, pageElement);
}
function applyHeadEntries(headEntries = [], doc = document) {
for (const node of doc.head.querySelectorAll("[data-haven-head='true']")) {
node.remove();
}
const normalizedEntries = normalizeHeadEntries(headEntries);
for (const entry of normalizedEntries) {
const node = doc.createElement(entry.tag);
node.setAttribute("data-haven-head", "true");
if (entry.tag === "meta") {
if (entry.name) {
node.setAttribute("name", entry.name);
}
if (entry.property) {
node.setAttribute("property", entry.property);
}
node.setAttribute("content", entry.content);
} else if (entry.tag === "link") {
node.setAttribute("rel", entry.rel);
node.setAttribute("href", entry.href);
}
doc.head.appendChild(node);
}
}
function originFromUrl(value) {
if (!value) {
return null;
}
const match = String(value).match(/^(https?:\/\/[^/]+)/i);
return match ? match[1] : null;
}
function readEnvelopeFromDocument(doc = document) {
const pageNode = doc.getElementById("__RUNTIME_PAGE__");
if (!pageNode?.textContent) {
throw new Error("Missing __RUNTIME_PAGE__ payload");
}
return JSON.parse(pageNode.textContent);
}
function syncDocument(envelope) {
globalThis.__RUNTIME_PAGE__ = envelope;
const pageNode = document.getElementById("__RUNTIME_PAGE__");
if (pageNode) {
pageNode.textContent = JSON.stringify(envelope);
}
document.title = pageTitle(envelope);
applyHeadEntries(envelope.meta?.head ?? []);
}
function normalizePartialOnly(only = []) {
return only
.map((entry) => {
if (!entry?.kind || !entry?.key) {
return null;
}
if (entry.kind !== "props" && entry.kind !== "resources") {
return null;
}
return {
kind: entry.kind,
key: String(entry.key),
};
})
.filter(Boolean);
}
function mergePartialEnvelope(currentEnvelope, partialEnvelope) {
if (!currentEnvelope) {
throw new Error("Cannot merge partial page data without a current envelope");
}
if (currentEnvelope.page?.component !== partialEnvelope.component) {
throw new Error("Partial reload component mismatch");
}
return {
...currentEnvelope,
page: {
...(currentEnvelope.page ?? {}),
component: partialEnvelope.component,
props: {
...(currentEnvelope.page?.props ?? {}),
...(partialEnvelope.props ?? {}),
},
url: partialEnvelope.url ?? currentEnvelope.page?.url,
version: partialEnvelope.version ?? currentEnvelope.page?.version,
locale: partialEnvelope.locale ?? currentEnvelope.page?.locale,
},
meta: {
...(currentEnvelope.meta ?? {}),
title:
partialEnvelope.title !== undefined
? partialEnvelope.title
: currentEnvelope.meta?.title,
resources: {
...(currentEnvelope.meta?.resources ?? {}),
...(partialEnvelope.resources ?? {}),
},
},
errors:
partialEnvelope.errors != null
? partialEnvelope.errors
: (currentEnvelope.errors ?? {}),
};
}
function normalizeErrors(errors) {
if (!errors || typeof errors !== "object" || Array.isArray(errors)) {
return {};
}
return errors;
}
function normalizePageContextValue(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
return value;
}
function pageContextFromEnvelope(envelope) {
return normalizePageContextValue(envelope?.page?.props?._context);
}
function resolveCsrfToken(page) {
const context = pageContextFromEnvelope(page);
return (
context.csrfToken ??
context.csrf_token ??
page?.page?.props?._auth?.csrf_token ??
null
);
}
function sanitizeEnvelope(envelope) {
return {
...envelope,
errors: normalizeErrors(envelope?.errors),
};
}
function hasFiles(value) {
if (!value) {
return false;
}
if (
isBinaryFormValue(value)
) {
return true;
}
if (isFileListInstance(value)) {
return value.length > 0;
}
if (Array.isArray(value)) {
return value.some(hasFiles);
}
if (typeof value === "object") {
return Object.values(value).some(hasFiles);
}
return false;
}
function appendFormValue(formData, key, value) {
if (value == null) {
formData.append(key, "");
return;
}
if (isBinaryFormValue(value)) {
formData.append(key, value);
return;
}
if (value instanceof Date) {
formData.append(key, value.toISOString());
return;
}
if (Array.isArray(value)) {
for (const entry of value) {
appendFormValue(formData, `${key}[]`, entry);
}
return;
}
if (typeof value === "object") {
formData.append(key, JSON.stringify(value));
return;
}
formData.append(key, String(value));
}
function objectToFormData(data = {}) {
const formData = new FormData();
for (const [key, value] of Object.entries(data)) {
appendFormValue(formData, key, value);
}
return formData;
}
function serializeQuery(data = {}) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
if (value == null) {
continue;
}
if (Array.isArray(value)) {
for (const entry of value) {
if (entry != null) {
params.append(key, String(entry));
}
}
continue;
}
if (typeof value === "object") {
params.set(key, JSON.stringify(value));
continue;
}
params.set(key, String(value));
}
return params;
}
function formDataHasFiles(formData) {
for (const value of formData.values()) {
if (
isBinaryFormValue(value)
) {
return true;
}
}
return false;
}
function sanitizeRememberedFormData(data = {}) {
const next = {};
for (const [key, value] of Object.entries(data)) {
if (value == null) {
next[key] = value;
continue;
}
if (
isBinaryFormValue(value)
|| isFileListInstance(value)
) {
continue;
}
if (Array.isArray(value)) {
next[key] = value.filter(
(entry) =>
!(
isBinaryFormValue(entry)
),
);
continue;
}
next[key] = value;
}
return next;
}
function formDataToObject(formData) {
const result = {};
for (const [key, value] of formData.entries()) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
const current = result[key];
result[key] = Array.isArray(current) ? [...current, value] : [current, value];
continue;
}
result[key] = value;
}
return result;
}
function runtimeClientAssets() {
const config = runtimeConfig();
const clientAssets = config.clientAssets ?? {};
return {
entryScriptUrl:
clientAssets.entryScriptUrl ?? config.assetScriptUrl ?? "/assets/entry-client.js",
modulePreloadUrls: clientAssets.modulePreloadUrls ?? [],
styleUrls: clientAssets.styleUrls ?? [],
viteDevClientUrl: config.viteDevClientUrl,
};
}
function renderViteReactPreamble(viteDevClientUrl) {
const viteDevOrigin = originFromUrl(viteDevClientUrl);
if (!viteDevOrigin) {
return "";
}
return `<script type="module">
import RefreshRuntime from "${escapeHtml(`${viteDevOrigin}/@react-refresh`)}";
RefreshRuntime.injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
window.__vite_plugin_react_preamble_installed__ = true;
</script>`;
}
function renderHeadAssetTags() {
const { modulePreloadUrls, styleUrls } = runtimeClientAssets();
return [
...styleUrls.map(
(url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`,
),
...modulePreloadUrls.map(
(url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`,
),
].join("\n ");
}
function renderBodyAssetTags() {
const { entryScriptUrl, viteDevClientUrl } = runtimeClientAssets();
const viteReactPreamble = renderViteReactPreamble(viteDevClientUrl);
return [
viteReactPreamble,
viteDevClientUrl
? `<script type="module" src="${escapeHtml(viteDevClientUrl)}"></script>`
: "",
entryScriptUrl
? `<script type="module" src="${escapeHtml(entryScriptUrl)}"></script>`
: "",
]
.filter(Boolean)
.join("\n ");
}
function renderDocument({ appHtml, envelope, head = [] }) {
const pageJson = serializePageEnvelope(envelope);
const headAssetTags = renderHeadAssetTags();
const bodyAssetTags = renderBodyAssetTags();
const managedHeadTags = renderManagedHeadTags(envelope.meta?.head ?? []);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>${escapeHtml(pageTitle(envelope))}</title>
${head.join("\n ")}
${managedHeadTags}
${headAssetTags}
</head>
<body>
<div id="app">${appHtml}</div>
<script id="__RUNTIME_PAGE__" type="application/json">${pageJson}</script>
${bodyAssetTags}
</body>
</html>`;
}
function renderDocumentPrefix({ envelope, head = [] }) {
const headAssetTags = renderHeadAssetTags();
const managedHeadTags = renderManagedHeadTags(envelope.meta?.head ?? []);
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>${escapeHtml(pageTitle(envelope))}</title>
${head.join("\n ")}
${managedHeadTags}
${headAssetTags}
</head>
<body>
<div id="app">`;
}
function renderDocumentSuffix({ envelope }) {
const pageJson = serializePageEnvelope(envelope);
const bodyAssetTags = renderBodyAssetTags();
return `</div>
<script id="__RUNTIME_PAGE__" type="application/json">${pageJson}</script>
${bodyAssetTags}
</body>
</html>`;
}
async function readStreamToString(stream) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let html = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
if (typeof value === "string") {
html += value;
} else if (value) {
html += decoder.decode(value, { stream: true });
}
}
html += decoder.decode();
return html;
}
export function usePage() {
const envelope = useContext(PageContext);
if (!envelope) {
throw new Error("usePage must be used inside a haven page app");
}
return envelope;
}
export function usePageContext() {
return pageContextFromEnvelope(usePage());
}
export function useRuntimeConfig() {
return runtimeConfig();
}
export function useLocale() {
const page = usePage();
return page.page?.locale ?? useRuntimeConfig().locale ?? "en";
}
export function useCsrfToken() {
const page = usePage();
return resolveCsrfToken(page);
}
export function useRouter() {
return useContext(RouterContext);
}
export function useNavigate() {
const router = useRouter();
if (!router) {
throw new Error("useNavigate must be used inside a haven page app");
}
return router.visit;
}
export function useReload() {
const router = useRouter();
if (!router) {
throw new Error("useReload must be used inside a haven page app");
}
return router.reload;
}
export function useNavigation() {
const router = useRouter();
if (!router) {
throw new Error("useNavigation must be used inside a haven page app");
}
return router.navigation ?? { ...DEFAULT_NAVIGATION_STATE };
}
export function useProgressIndicator() {
return useNavigation();
}
export function usePrefetch() {
const router = useRouter();
if (!router) {
throw new Error("usePrefetch must be used inside a haven page app");
}
return router.prefetch;
}
export function useRemember(initialValue, key) {
const router = useRouter();
const unset = Symbol.for("haven.remember.unset");
const initialValueRef = useRef(unset);
const keyRef = useRef(key);
if (!router) {
throw new Error("useRemember must be used inside a haven page app");
}
if (keyRef.current !== key) {
keyRef.current = key;
initialValueRef.current = unset;
}
if (initialValueRef.current === unset) {
initialValueRef.current =
typeof initialValue === "function" ? initialValue() : initialValue;
}
const hasRememberedValue = Object.prototype.hasOwnProperty.call(router.state ?? {}, key);
const value = hasRememberedValue ? router.state[key] : initialValueRef.current;
function setValue(nextValue) {
const resolvedValue =
typeof nextValue === "function" ? nextValue(value) : nextValue;
return router.remember(key, resolvedValue);
}
return [value, setValue];
}
export function useForm(initialData, options = {}) {
const router = useRouter();
const page = usePage();
const initialDefaultsRef = useRef(
typeof initialData === "function" ? initialData() : (initialData ?? {}),
);
const [data, setDataState] = useState(() => {
if (options.remember) {
return router?.restore(
options.remember,
sanitizeRememberedFormData(initialDefaultsRef.current),
) ?? initialDefaultsRef.current;
}
return initialDefaultsRef.current;
});
const [processing, setProcessing] = useState(false);
const [progress, setProgress] = useState(null);
const [wasSuccessful, setWasSuccessful] = useState(false);
const [recentlySuccessful, setRecentlySuccessful] = useState(false);
const [errors, setErrors] = useState(() => normalizeErrors(page.errors));
const recentlySuccessfulTimeoutRef = useRef(null);
useEffect(() => {
setErrors(normalizeErrors(page.errors));
}, [page.errors]);
useEffect(() => () => {
if (recentlySuccessfulTimeoutRef.current) {
clearTimeout(recentlySuccessfulTimeoutRef.current);
}
}, []);
useEffect(() => {
if (options.remember && router) {
router.remember(options.remember, sanitizeRememberedFormData(data));
}
}, [data, options.remember]);
function setData(...args) {
if (args.length === 1) {
const [nextValue] = args;
setDataState((current) =>
typeof nextValue === "function" ? nextValue(current) : nextValue,
);
return;
}
const [key, value] = args;
setDataState((current) => ({
...current,
[key]: typeof value === "function" ? value(current[key]) : value,
}));
}
function reset(...keys) {
setDataState((current) => {
if (keys.length === 0) {
return initialDefaultsRef.current;
}
const next = { ...current };
for (const key of keys) {
next[key] = initialDefaultsRef.current[key];
}
return next;
});
}
function defaults(nextDefaults) {
initialDefaultsRef.current =
typeof nextDefaults === "function"
? nextDefaults(initialDefaultsRef.current)
: nextDefaults;
return initialDefaultsRef.current;
}
function clearErrors(...keys) {
if (keys.length === 0) {
setErrors({});
return;
}
setErrors((current) => {
const next = { ...current };
for (const key of keys) {
delete next[key];
}
return next;
});
}
function setError(key, value) {
setErrors((current) => ({
...current,
[key]: value,
}));
}
async function submit(method, url, submitOptions = {}) {
if (!router) {
throw new Error("useForm must be used inside a haven page app");
}
setProcessing(true);
setProgress(null);
setWasSuccessful(false);
setRecentlySuccessful(false);
clearErrors();
submitOptions.onStart?.();
try {
const nextEnvelope = await router.submit(method, url, {
data,
preserveState: submitOptions.preserveState ?? true,
preserveScroll: submitOptions.preserveScroll ?? true,
replace: submitOptions.replace ?? false,
only: submitOptions.only ?? [],
optimistic: submitOptions.optimistic,
onOptimistic: submitOptions.onOptimistic,
onProgress: (event) => {
setProgress(event);
submitOptions.onProgress?.(event);
},
onRollback: submitOptions.onRollback,
});
const currentErrors = normalizeErrors(nextEnvelope?.errors);
if (Object.keys(currentErrors).length > 0) {
setErrors(currentErrors);
submitOptions.onError?.(currentErrors);
} else if (nextEnvelope) {
setWasSuccessful(true);
setRecentlySuccessful(true);
if (recentlySuccessfulTimeoutRef.current) {
clearTimeout(recentlySuccessfulTimeoutRef.current);
}
recentlySuccessfulTimeoutRef.current = window.setTimeout(() => {
setRecentlySuccessful(false);
}, 2000);
submitOptions.onSuccess?.(nextEnvelope);
}
return nextEnvelope;
} finally {
setProcessing(false);
setProgress(null);
submitOptions.onFinish?.();
}
}
return {
data,
setData,
reset,
defaults,
submit,
get: (url, submitOptions = {}) => submit("GET", url, submitOptions),
post: (url, submitOptions = {}) => submit("POST", url, submitOptions),
put: (url, submitOptions = {}) => submit("PUT", url, submitOptions),
patch: (url, submitOptions = {}) => submit("PATCH", url, submitOptions),
delete: (url, submitOptions = {}) => submit("DELETE", url, submitOptions),
processing,
progress,
wasSuccessful,
recentlySuccessful,
errors,
hasErrors: Object.keys(errors).length > 0,
clearErrors,
setError,
};
}
export function definePageModule(module) {
const normalized = normalizePageModule(module);
if (!normalized?.component) {
throw new Error("definePageModule requires a component");
}
const Component = normalized.component;
Object.defineProperty(Component, "__havenPageModule", {
value: normalized,
configurable: true,
enumerable: false,
writable: true,
});
return Component;
}
export function definePages(modules) {
return modules;
}
export function pageNameFromModulePath(modulePath) {
if (!modulePath) {
throw new Error("pageNameFromModulePath requires a module path");
}
const normalized = modulePath.replaceAll("\\", "/");
const pagesIndex = normalized.indexOf("/pages/");
const relativePath = pagesIndex >= 0 ? normalized.slice(pagesIndex + 7) : normalized;
return relativePath.replace(/\.(jsx?|tsx?)$/, "");
}
export function definePagesFromGlob(globModules) {
const pages = {};
for (const [modulePath, moduleExports] of Object.entries(globModules)) {
const pageName = pageNameFromModulePath(modulePath);
const pageModule = moduleExports?.default ?? moduleExports;
pages[pageName] = pageModule;
}
return definePages(pages);
}
function normalizePageModule(module) {
if (typeof module === "function") {
if (module.__havenPageModule?.component) {
return module.__havenPageModule;
}
return {
component: module,
layouts: [],
};
}
if (!module?.component) {
return module;
}
return {
...module,
layouts: Array.isArray(module.layouts)
? module.layouts
: module.layout
? [module.layout]
: [],
};
}
function isModifiedEvent(event) {
return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey;
}
export function shouldInterceptNavigation(event, href, target, download) {
if (event.defaultPrevented || event.button !== 0 || isModifiedEvent(event)) {
return false;
}
return canPrefetchNavigation(href, target, download);
}
function canPrefetchNavigation(href, target, download) {
if (target && target !== "_self") {
return false;
}
if (download) {
return false;
}
if (!href || href.startsWith("#")) {
return false;
}
const url = new URL(href, window.location.href);
if (url.origin !== window.location.origin) {
return false;
}
return !url.pathname.startsWith("/assets/");
}
export function RouterProvider({ router, children }) {
return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>;
}
export function Link({
href,
replace = false,
onClick,
prefetch = false,
children,
...props
}) {
const router = useRouter();
const linkRef = useRef(null);
const hoverTimerRef = useRef(null);
async function handleClick(event) {
onClick?.(event);
if (!router || !shouldInterceptNavigation(event, href, props.target, props.download)) {
return;
}
event.preventDefault();
try {
await router.visit(href, { replace });
} catch (error) {
console.error("Client navigation failed; falling back to full reload.", error);
window.location.assign(href);
}
}
useEffect(() => {
if (!router || prefetch !== "visible" || typeof IntersectionObserver === "undefined") {
return undefined;
}
const node = linkRef.current;
if (!node || !canPrefetchNavigation(href, props.target, props.download)) {
return undefined;
}
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
router.prefetch(href).catch(() => {});
observer.disconnect();
break;
}
}
});
observer.observe(node);
return () => observer.disconnect();
}, [router, prefetch, href, props.target, props.download]);
useEffect(() => () => {
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
}
}, []);
function handleMouseEnter(event) {
props.onMouseEnter?.(event);
if (!router || (prefetch !== true && prefetch !== "hover")) {
return;
}
if (!shouldInterceptNavigation(event, href, props.target, props.download)) {
return;
}
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
}
hoverTimerRef.current = window.setTimeout(() => {
router.prefetch(href).catch(() => {});
}, 50);
}
function handleMouseLeave(event) {
props.onMouseLeave?.(event);
if (hoverTimerRef.current) {
clearTimeout(hoverTimerRef.current);
hoverTimerRef.current = null;
}
}
return (
<a
{...props}
ref={linkRef}
href={href}
data-haven-link="true"
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{children}
</a>
);
}
export function Form({
action,
method = "post",
csrf = true,
csrfToken: csrfTokenOverride,
csrfFieldName = "csrf_token",
onSubmit,
preserveState = true,
preserveScroll = true,
replace = false,
only = [],
optimistic,
onStart,
onOptimistic,
onProgress,
onRollback,
onSuccess,
onError,
onFinish,
children,
...props
}) {
const router = useRouter();
const page = useContext(PageContext);
const csrfToken = csrfTokenOverride ?? resolveCsrfToken(page);
const shouldIncludeCsrf =
Boolean(csrf) &&
Boolean(csrfToken) &&
String(method).toUpperCase() !== "GET" &&
Boolean(csrfFieldName);
async function handleSubmit(event) {
onSubmit?.(event);
if (event.defaultPrevented || !router) {
return;
}
event.preventDefault();
const form = event.currentTarget;
const submitter = event.nativeEvent?.submitter;
const formData = submitter ? new FormData(form, submitter) : new FormData(form);
const submitMethod = String(
submitter?.getAttribute?.("formmethod") ?? form.getAttribute("method") ?? method,
).toUpperCase();
const submitAction =
submitter?.getAttribute?.("formaction") ??
form.getAttribute("action") ??
action ??
window.location.href;
try {
await router.submit(submitMethod, submitAction, {
data: formDataToObject(formData),
formData,
preserveState,
preserveScroll,
replace,
only,
optimistic,
onStart,
onOptimistic,
onProgress,
onRollback,
onSuccess,
onError,
onFinish,
});
} catch (error) {
console.error("Form submission failed; falling back to full navigation.", error);
form.submit();
}
}
return (
<form
{...props}
action={action}
method={method}
onSubmit={handleSubmit}
>
{shouldIncludeCsrf ? (
<input type="hidden" name={csrfFieldName} value={csrfToken} />
) : null}
{children}
</form>
);
}
export function ProgressBar({
delay = 150,
color = "#111",
height = 3,
className,
style,
}) {
const navigation = useProgressIndicator();
if (!navigation.visible) {
return null;
}
return (
<div
aria-hidden="true"
className={className}
style={{
position: "fixed",
inset: 0,
bottom: "auto",
height,
width: "100%",
zIndex: 2147483647,
pointerEvents: "none",
background: "transparent",
...style,
}}
>
<div
style={{
height: "100%",
width: `${navigation.progress}%`,
background: color,
transition: navigation.pending ? "width 160ms ease-out" : "width 120ms ease-out",
boxShadow: `0 0 8px ${color}`,
}}
/>
</div>
);
}
function protocolHeaders(currentEnvelope, only = []) {
const currentVersion = currentEnvelope?.page?.version ?? runtimeConfig().version ?? "dev";
const headers = {
Accept: "application/json, text/html;q=0.9,application/xhtml+xml;q=0.8",
"X-Haven-Visit": "true",
"X-Haven-Version": currentVersion,
};
if (only.length > 0 && currentEnvelope?.page?.component) {
headers["X-Haven-Partial-Component"] = currentEnvelope.page.component;
headers["X-Haven-Partial-Only"] = only.map((entry) => entry.key).join(",");
headers["X-Haven-Partial-Kind"] = only.map((entry) => entry.kind).join(",");
}
return headers;
}
function parseEnvelopePayload(payload, contentType, currentEnvelope, isPartial) {
if (contentType.includes("application/json")) {
const parsed = JSON.parse(payload);
return isPartial ? mergePartialEnvelope(currentEnvelope, parsed) : parsed;
}
const nextDocument = new DOMParser().parseFromString(payload, "text/html");
return readEnvelopeFromDocument(nextDocument);
}
async function parseEnvelopeFetchResponse(response, options = {}) {
if (response.status === 409) {
const location = response.headers.get("x-haven-location");
if (location) {
if (options.prefetch) {
return null;
}
window.location.assign(location);
return null;
}
}
if (options.prefetch && response.redirected) {
return null;
}
const contentType = response.headers.get("content-type") ?? "";
const payload = await response.text();
const envelope = sanitizeEnvelope(
parseEnvelopePayload(
payload,
contentType,
options.currentEnvelope,
response.headers.get("x-haven-partial") === "true",
),
);
return {
envelope,
status: response.status,
redirected: response.redirected,
};
}
export async function fetchPageEnvelope(url, options = {}) {
const only = normalizePartialOnly(options.only ?? []);
const headers = protocolHeaders(options.currentEnvelope, only);
const response = await fetch(url, {
headers,
credentials: "same-origin",
signal: options.signal,
});
if (!response.ok) {
throw new Error(`Navigation failed with status ${response.status}`);
}
const parsed = await parseEnvelopeFetchResponse(response, options);
return parsed?.envelope ?? null;
}
export async function prefetchPage(url, options = {}) {
const version = currentPageVersion(options.currentEnvelope);
const cachedEnvelope = getCachedEnvelope(url, version);
if (cachedEnvelope) {
return cachedEnvelope;
}
const key = envelopeCacheKey(url, version);
const pending = inflightPrefetches().get(key);
if (pending) {
return pending;
}
const controller = new AbortController();
const prefetchPromise = fetchPageEnvelope(url, {
currentEnvelope: options.currentEnvelope,
prefetch: true,
signal: options.signal ?? controller.signal,
})
.then((envelope) => {
inflightPrefetches().delete(key);
if (envelope) {
return setCachedEnvelope(url, version, envelope);
}
return envelope;
})
.catch((error) => {
inflightPrefetches().delete(key);
if (error?.name === "AbortError") {
return null;
}
throw error;
});
inflightPrefetches().set(key, prefetchPromise);
return prefetchPromise;
}
function submitViaXhr(url, method, formData, options = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.withCredentials = true;
for (const [name, value] of Object.entries(options.headers ?? {})) {
xhr.setRequestHeader(name, value);
}
xhr.onload = () => {
const contentType = xhr.getResponseHeader("content-type") ?? "";
const isPartial = xhr.getResponseHeader("x-haven-partial") === "true";
const location = xhr.getResponseHeader("x-haven-location");
if (xhr.status === 409 && location) {
window.location.assign(location);
resolve(null);
return;
}
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 422) {
resolve({
envelope: sanitizeEnvelope(
parseEnvelopePayload(
xhr.responseText,
contentType,
options.currentEnvelope,
isPartial,
),
),
status: xhr.status,
});
return;
}
reject(new Error(`Form submission failed with status ${xhr.status}`));
};
xhr.onerror = () => reject(new Error("Form submission failed"));
xhr.onabort = () => reject(new DOMException("Aborted", "AbortError"));
if (options.signal) {
options.signal.addEventListener("abort", () => xhr.abort(), { once: true });
}
if (xhr.upload && options.onProgress) {
xhr.upload.onprogress = (event) => {
if (!event.lengthComputable) {
return;
}
options.onProgress({
loaded: event.loaded,
total: event.total,
percentage: Math.round((event.loaded / event.total) * 100),
});
};
}
xhr.send(formData);
});
}
export async function submitPageForm(url, options = {}) {
const method = String(options.method ?? "POST").toUpperCase();
const only = normalizePartialOnly(options.only ?? []);
const headers = protocolHeaders(options.currentEnvelope, only);
const baseUrl = new URL(url, window.location.href);
if (method === "GET") {
const query = serializeQuery(options.data ?? {});
query.forEach((value, key) => {
baseUrl.searchParams.delete(key);
baseUrl.searchParams.append(key, value);
});
const response = await fetch(baseUrl.href, {
method,
headers,
credentials: "same-origin",
signal: options.signal,
});
const parsed = await parseEnvelopeFetchResponse(response, options);
if (!parsed) {
return null;
}
if (!response.ok) {
throw new Error(`Form submission failed with status ${response.status}`);
}
return parsed;
}
const formData = options.formData ?? objectToFormData(options.data ?? {});
const useXhr = formDataHasFiles(formData);
if (useXhr) {
return submitViaXhr(baseUrl.href, method, formData, {
headers,
signal: options.signal,
currentEnvelope: options.currentEnvelope,
onProgress: options.onProgress,
});
}
const response = await fetch(baseUrl.href, {
method,
headers,
credentials: "same-origin",
body: formData,
signal: options.signal,
});
const parsed = await parseEnvelopeFetchResponse(response, options);
if (!parsed) {
return null;
}
if (!(response.ok || response.status === 422)) {
throw new Error(`Form submission failed with status ${response.status}`);
}
return parsed;
}
export function createPageRenderer({ pages, renderLayout }) {
function resolvePageModule(component) {
const page = pages[component];
if (!page) {
throw new Error(`Unknown component: ${component}`);
}
const normalized = normalizePageModule(page);
if (!normalized?.component) {
throw new Error(`Invalid page module: ${component}`);
}
return normalized;
}
function resolvePageComponent(component) {
return resolvePageModule(component).component;
}
function resolveDocumentEnvelope(envelope) {
const pageModule = resolvePageModule(envelope.page.component);
const resolvedHead =
envelope.meta?.head?.length > 0
? normalizeHeadEntries(envelope.meta.head)
: normalizeHeadEntries(
typeof pageModule.head === "function"
? pageModule.head({
props: envelope.page?.props ?? {},
page: envelope.page,
meta: envelope.meta ?? {},
})
: pageModule.head ?? [],
);
return {
...sanitizeEnvelope(envelope),
meta: {
...(envelope.meta ?? {}),
title: envelope.meta?.title ?? pageModule.title,
head: resolvedHead,
},
};
}
function AppFrame({ envelope, router = null }) {
const documentEnvelope = resolveDocumentEnvelope(envelope);
globalThis.__RUNTIME_PAGE__ = documentEnvelope;
const runtimeRouter =
router ??
{
...DEFAULT_SERVER_ROUTER,
envelope: documentEnvelope,
currentUrl: documentEnvelope.page?.url ?? "/",
remember() {
throw new Error("router.remember is not available during server rendering");
},
restore(_key, fallback) {
return fallback;
},
visit() {
throw new Error("router.visit is not available during server rendering");
},
submit() {
throw new Error("router.submit is not available during server rendering");
},
reload() {
throw new Error("router.reload is not available during server rendering");
},
};
const pageModule = resolvePageModule(documentEnvelope.page.component);
const Component = pageModule.component;
const pageElement = <Component {...(documentEnvelope.page.props ?? {})} />;
const layoutInput = {
envelope: documentEnvelope,
page: documentEnvelope.page,
meta: documentEnvelope.meta,
component: Component,
module: pageModule,
children: pageElement,
};
const moduleChildren = applyLayouts(pageModule.layouts, layoutInput, pageElement);
const children = renderLayout
? renderLayout({
...layoutInput,
children: moduleChildren,
})
: moduleChildren;
return (
<PageContext.Provider value={documentEnvelope}>
<RouterProvider router={runtimeRouter}>{children}</RouterProvider>
</PageContext.Provider>
);
}
async function renderEnvelope(envelope) {
const documentEnvelope = resolveDocumentEnvelope(envelope);
globalThis.__RUNTIME_PAGE__ = documentEnvelope;
runtimeBridge().log(
"info",
`rendering ${documentEnvelope.page.component} at ${runtimeBridge().now()}`,
);
const stream = await renderToReadableStream(<AppFrame envelope={documentEnvelope} />);
if (stream.allReady) {
await stream.allReady;
}
const appHtml = await readStreamToString(stream);
const head = [
`<meta name="renderer-version" content="${escapeHtml(documentEnvelope.page.version ?? "dev")}">`,
];
return {
html: renderDocument({ appHtml, envelope: documentEnvelope, head }),
head,
status: 200,
};
}
async function streamEnvelope(envelope) {
const documentEnvelope = resolveDocumentEnvelope(envelope);
globalThis.__RUNTIME_PAGE__ = documentEnvelope;
runtimeBridge().log(
"info",
`streaming ${documentEnvelope.page.component} at ${runtimeBridge().now()}`,
);
const head = [
`<meta name="renderer-version" content="${escapeHtml(documentEnvelope.page.version ?? "dev")}">`,
];
const prefix = renderDocumentPrefix({ envelope: documentEnvelope, head });
const suffix = renderDocumentSuffix({ envelope: documentEnvelope });
globalThis.__RUNTIME_STREAM__.write(prefix);
const stream = await renderToReadableStream(<AppFrame envelope={documentEnvelope} />);
const reader = stream.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
if (typeof value === "string") {
globalThis.__RUNTIME_STREAM__.write(value);
} else if (value) {
globalThis.__RUNTIME_STREAM__.write(decoder.decode(value, { stream: true }));
}
}
globalThis.__RUNTIME_STREAM__.write(decoder.decode());
globalThis.__RUNTIME_STREAM__.write(suffix);
return {
html: "",
head,
status: 200,
};
}
AppFrame.resolveDocumentEnvelope = resolveDocumentEnvelope;
return {
AppFrame,
resolvePageModule,
resolveDocumentEnvelope,
renderEnvelope,
streamEnvelope,
resolvePageComponent,
};
}
export function defineServerEntry({ renderEnvelope, streamEnvelope }) {
if (typeof renderEnvelope !== "function") {
throw new TypeError("defineServerEntry requires a renderEnvelope function");
}
if (typeof streamEnvelope !== "function") {
throw new TypeError("defineServerEntry requires a streamEnvelope function");
}
globalThis.__RUNTIME_RENDER__ = renderEnvelope;
globalThis.__RUNTIME_RENDER_STREAM__ = streamEnvelope;
return {
renderPage: renderEnvelope,
streamPage: streamEnvelope,
};
}
export function hydratePageApp({ AppFrame }) {
const appNode = document.getElementById("app");
if (!appNode) {
return null;
}
const resolveEnvelope = AppFrame.resolveDocumentEnvelope ?? ((envelope) => envelope);
const initialEnvelope = resolveEnvelope(sanitizeEnvelope(readEnvelopeFromDocument()));
function ClientApp({ initialPage }) {
const [envelope, setEnvelope] = useState(() => initialPage);
const [pageState, setPageState] = useState(() => window.history.state?.state ?? {});
const [navigation, setNavigation] = useState({
pending: false,
progress: 0,
visible: false,
targetUrl: null,
navigationType: null,
});
const navigationIdRef = useRef(0);
const navigationAbortRef = useRef(null);
const progressDelayRef = useRef(null);
const progressTickRef = useRef(null);
const progressHideRef = useRef(null);
const prefetchAbortControllersRef = useRef(new Map());
const pageStateRef = useRef(pageState);
function clearProgressTimers() {
if (progressDelayRef.current) {
clearTimeout(progressDelayRef.current);
progressDelayRef.current = null;
}
if (progressTickRef.current) {
clearInterval(progressTickRef.current);
progressTickRef.current = null;
}
if (progressHideRef.current) {
clearTimeout(progressHideRef.current);
progressHideRef.current = null;
}
}
function abortPrefetch(url) {
const controller = prefetchAbortControllersRef.current.get(url);
if (controller) {
controller.abort();
prefetchAbortControllersRef.current.delete(url);
}
}
function abortActiveNavigation() {
if (navigationAbortRef.current) {
navigationAbortRef.current.abort();
navigationAbortRef.current = null;
}
}
function beginNavigationProgress(targetUrl, navigationType) {
navigationIdRef.current += 1;
const navigationId = navigationIdRef.current;
clearProgressTimers();
setNavigation({
pending: true,
progress: 0,
visible: false,
targetUrl,
navigationType,
});
progressDelayRef.current = window.setTimeout(() => {
if (navigationIdRef.current !== navigationId) {
return;
}
setNavigation((current) => ({
...current,
visible: true,
progress: current.progress > 0 ? current.progress : 12,
}));
progressTickRef.current = window.setInterval(() => {
if (navigationIdRef.current !== navigationId) {
return;
}
setNavigation((current) => {
if (!current.pending) {
return current;
}
const nextProgress = Math.min(
90,
current.progress + Math.max(3, (92 - current.progress) * 0.12),
);
return {
...current,
progress: nextProgress,
};
});
}, 120);
}, 150);
return navigationId;
}
function completeNavigationProgress(navigationId) {
if (navigationIdRef.current !== navigationId) {
return;
}
clearProgressTimers();
setNavigation((current) => {
if (!current.visible) {
return {
pending: false,
progress: 0,
visible: false,
targetUrl: null,
navigationType: null,
};
}
return {
...current,
pending: false,
progress: 100,
};
});
progressHideRef.current = window.setTimeout(() => {
if (navigationIdRef.current !== navigationId) {
return;
}
setNavigation({
pending: false,
progress: 0,
visible: false,
targetUrl: null,
navigationType: null,
});
}, 180);
}
function failNavigationProgress(navigationId) {
if (navigationIdRef.current !== navigationId) {
return;
}
clearProgressTimers();
setNavigation({
pending: false,
progress: 0,
visible: false,
targetUrl: null,
navigationType: null,
});
}
function setCurrentState(nextState) {
pageStateRef.current = nextState;
setPageState(nextState);
}
function replaceCurrentHistoryState(nextState, nextEnvelope = envelope) {
const currentUrl = nextEnvelope?.page?.url
? new URL(nextEnvelope.page.url, window.location.href).href
: window.location.href;
window.history.replaceState(
{ envelope: nextEnvelope, state: nextState },
"",
currentUrl,
);
}
function commitNavigationResult(resolvedEnvelope, destinationUrl, options = {}) {
startTransition(() => {
setEnvelope(resolvedEnvelope);
});
const nextUrl = resolvedEnvelope?.page?.url
? new URL(resolvedEnvelope.page.url, window.location.href).href
: destinationUrl;
const historyState = {
envelope: resolvedEnvelope,
state: pageStateRef.current,
};
if (options.replace) {
window.history.replaceState(historyState, "", nextUrl);
} else {
window.history.pushState(historyState, "", nextUrl);
}
if (!options.preserveScroll) {
window.scrollTo(0, 0);
}
}
function rollbackOptimisticNavigation(snapshotEnvelope, options, navigationId) {
if (navigationIdRef.current !== navigationId) {
return;
}
startTransition(() => {
setEnvelope(snapshotEnvelope);
});
replaceCurrentHistoryState(pageStateRef.current, snapshotEnvelope);
options.onRollback?.(snapshotEnvelope);
}
function applyOptimisticNavigation(snapshotEnvelope, options) {
if (typeof options.optimistic !== "function") {
return null;
}
const optimisticEnvelope = resolveEnvelope(
sanitizeEnvelope(
options.optimistic({
envelope: snapshotEnvelope,
data: options.data ?? formDataToObject(options.formData ?? new FormData()),
}),
),
);
startTransition(() => {
setEnvelope(optimisticEnvelope);
});
replaceCurrentHistoryState(pageStateRef.current, optimisticEnvelope);
options.onOptimistic?.(optimisticEnvelope);
return optimisticEnvelope;
}
function finalizeSubmitNavigation(
result,
destinationUrl,
options,
snapshotEnvelope,
optimisticEnvelope,
navigationId,
) {
const resolvedEnvelope = resolveEnvelope(result.envelope);
if (navigationIdRef.current !== navigationId) {
return null;
}
if (optimisticEnvelope && result.status === 422) {
options.onRollback?.(snapshotEnvelope);
}
commitNavigationResult(resolvedEnvelope, destinationUrl, {
...options,
replace: result.status === 422 || options.replace,
});
navigationAbortRef.current = null;
completeNavigationProgress(navigationId);
return resolvedEnvelope;
}
function remember(key, value) {
const nextState = {
...(pageStateRef.current ?? {}),
[key]: value,
};
setCurrentState(nextState);
replaceCurrentHistoryState(nextState);
return value;
}
function restore(key, fallback) {
if (Object.prototype.hasOwnProperty.call(pageStateRef.current ?? {}, key)) {
return pageStateRef.current[key];
}
return fallback;
}
useEffect(() => {
syncDocument(envelope);
}, [envelope]);
useEffect(() => {
pageStateRef.current = pageState;
}, [pageState]);
useEffect(() => {
globalThis.__HAVEN_HYDRATED__ = true;
return () => {
globalThis.__HAVEN_HYDRATED__ = false;
};
}, []);
useEffect(() => () => {
clearProgressTimers();
abortActiveNavigation();
for (const controller of prefetchAbortControllersRef.current.values()) {
controller.abort();
}
prefetchAbortControllersRef.current.clear();
}, []);
useEffect(() => {
let isMounted = true;
async function handlePopState(event) {
if (event.state?.envelope) {
abortActiveNavigation();
failNavigationProgress(navigationIdRef.current);
setCurrentState(event.state.state ?? {});
startTransition(() => {
setEnvelope(event.state.envelope);
});
return;
}
abortActiveNavigation();
const controller = new AbortController();
navigationAbortRef.current = controller;
try {
const navigationId = beginNavigationProgress(window.location.href, "popstate");
const nextEnvelope = await fetchPageEnvelope(window.location.href, {
signal: controller.signal,
});
if (nextEnvelope == null) {
completeNavigationProgress(navigationId);
return;
}
const resolvedEnvelope = resolveEnvelope(nextEnvelope);
if (!isMounted || navigationIdRef.current !== navigationId) {
return;
}
const nextState = event.state?.state ?? {};
setCurrentState(nextState);
startTransition(() => {
setEnvelope(resolvedEnvelope);
});
replaceCurrentHistoryState(nextState, resolvedEnvelope);
navigationAbortRef.current = null;
completeNavigationProgress(navigationId);
} catch (error) {
if (error?.name === "AbortError") {
return;
}
failNavigationProgress(navigationIdRef.current);
console.error("History navigation failed; reloading page.", error);
window.location.reload();
}
}
window.addEventListener("popstate", handlePopState);
window.history.replaceState(
{ envelope: initialPage, state: pageStateRef.current },
"",
window.location.href,
);
return () => {
isMounted = false;
window.removeEventListener("popstate", handlePopState);
};
}, [initialPage]);
async function visit(url, options = {}) {
const destination = new URL(url, window.location.href);
abortActiveNavigation();
abortPrefetch(destination.href);
const controller = new AbortController();
navigationAbortRef.current = controller;
const navigationId = beginNavigationProgress(
destination.href,
options.navigationType ?? "visit",
);
try {
const version = currentPageVersion(envelope);
let nextEnvelope = getCachedEnvelope(destination.href, version);
if (!nextEnvelope) {
const prefetchKey = envelopeCacheKey(destination.href, version);
const inflightPrefetch = inflightPrefetches().get(prefetchKey);
nextEnvelope = inflightPrefetch
? await inflightPrefetch
: await fetchPageEnvelope(destination.href, {
currentEnvelope: envelope,
only: options.only,
signal: controller.signal,
});
}
if (nextEnvelope == null) {
completeNavigationProgress(navigationId);
return null;
}
const resolvedEnvelope = resolveEnvelope(nextEnvelope);
if (navigationIdRef.current !== navigationId) {
return null;
}
commitNavigationResult(resolvedEnvelope, destination.href, options);
navigationAbortRef.current = null;
completeNavigationProgress(navigationId);
return resolvedEnvelope;
} catch (error) {
if (error?.name === "AbortError") {
return null;
}
failNavigationProgress(navigationId);
throw error;
}
}
async function submit(method, url, options = {}) {
const destination = new URL(url, window.location.href);
abortActiveNavigation();
const controller = new AbortController();
navigationAbortRef.current = controller;
const navigationId = beginNavigationProgress(
destination.href,
options.navigationType ?? "submit",
);
const snapshotEnvelope = envelope;
let optimisticEnvelope = null;
try {
options.onStart?.();
optimisticEnvelope = applyOptimisticNavigation(snapshotEnvelope, options);
const result = await submitPageForm(destination.href, {
method,
data: options.data,
formData: options.formData,
currentEnvelope: envelope,
only: options.only,
signal: controller.signal,
onProgress: options.onProgress,
});
if (result == null) {
completeNavigationProgress(navigationId);
return null;
}
return finalizeSubmitNavigation(
result,
destination.href,
options,
snapshotEnvelope,
optimisticEnvelope,
navigationId,
);
} catch (error) {
if (error?.name === "AbortError") {
if (optimisticEnvelope) {
rollbackOptimisticNavigation(snapshotEnvelope, options, navigationId);
}
return null;
}
if (optimisticEnvelope) {
rollbackOptimisticNavigation(snapshotEnvelope, options, navigationId);
}
failNavigationProgress(navigationId);
throw error;
} finally {
options.onFinish?.();
}
}
async function reload(options = {}) {
return visit(window.location.href, {
replace: options.replace ?? true,
preserveState: options.preserveState ?? true,
preserveScroll: options.preserveScroll ?? true,
only: options.only ?? [],
navigationType: "reload",
});
}
async function prefetch(url, options = {}) {
const destination = absolutePageUrl(url);
const version = currentPageVersion(envelope);
const cachedEnvelope = getCachedEnvelope(destination, version);
if (cachedEnvelope) {
return cachedEnvelope;
}
if (prefetchAbortControllersRef.current.has(destination)) {
return inflightPrefetches().get(envelopeCacheKey(destination, version)) ?? null;
}
const controller = new AbortController();
prefetchAbortControllersRef.current.set(destination, controller);
try {
return await prefetchPage(destination, {
currentEnvelope: envelope,
signal: controller.signal,
...options,
});
} finally {
prefetchAbortControllersRef.current.delete(destination);
}
}
useEffect(() => {
async function handleDocumentClick(event) {
const anchor = event.target instanceof Element
? event.target.closest("a[data-haven-link='true']")
: null;
if (!anchor) {
return;
}
if (
!shouldInterceptNavigation(
event,
anchor.getAttribute("href"),
anchor.getAttribute("target"),
anchor.hasAttribute("download"),
)
) {
return;
}
event.preventDefault();
try {
await visit(anchor.href);
} catch (error) {
console.error("Delegated navigation failed; falling back to full reload.", error);
window.location.assign(anchor.href);
}
}
document.addEventListener("click", handleDocumentClick);
return () => {
document.removeEventListener("click", handleDocumentClick);
};
});
const router = {
envelope,
currentUrl: envelope.page?.url ?? window.location.pathname,
navigation,
state: pageState,
prefetch,
remember,
restore,
submit,
visit,
reload,
};
return <AppFrame envelope={envelope} router={router} />;
}
return hydrateRoot(appNode, <ClientApp initialPage={initialEnvelope} />);
}