@takazudo/zfb-runtime 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -145,9 +145,13 @@ export function prefetch(url, opts = {}) {
145
145
  return;
146
146
  executePrefetch(href, opts).catch(() => { });
147
147
  }
148
+ // Capture listeners also receive events targeted at document or text nodes.
149
+ function closestLink(target) {
150
+ return target instanceof Element ? target.closest("a[href]") : null;
151
+ }
148
152
  function makeEnterLeaveHandlers(cancelHandles) {
149
153
  function enterHandler(e) {
150
- const link = e.target.closest("a[href]");
154
+ const link = closestLink(e.target);
151
155
  if (!link)
152
156
  return;
153
157
  if (!shouldPrefetchLink(link, "hover"))
@@ -188,7 +192,7 @@ function makeEnterLeaveHandlers(cancelHandles) {
188
192
  }
189
193
  }
190
194
  function leaveHandler(e) {
191
- const link = e.target.closest("a[href]");
195
+ const link = closestLink(e.target);
192
196
  if (!link)
193
197
  return;
194
198
  // The pointer/focus is moving to relatedTarget. If that's still inside
@@ -222,7 +226,7 @@ const [onFocusIn, onFocusOut] = makeEnterLeaveHandlers(focusCancelHandles);
222
226
  // Trigger: tap (touchstart / mousedown)
223
227
  // ---------------------------------------------------------------------------
224
228
  function onTap(e) {
225
- const link = e.target.closest("a[href]");
229
+ const link = closestLink(e.target);
226
230
  if (!link)
227
231
  return;
228
232
  if (!shouldPrefetchLink(link, "tap"))
@@ -1 +1 @@
1
- {"version":3,"file":"prefetch.js","sourceRoot":"","sources":["../../src/client-router/prefetch.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n/// <reference lib=\"dom.iterable\" />\n// `@takazudo/zfb-runtime` — prefetch module.\n//\n// Ported from Astro's prefetch module (packages/astro/src/prefetch/index.ts).\n// Source: https://github.com/withastro/astro\n// Issue: Takazudo/zudo-front-builder#276\n//\n// Mechanical renames per W1B §13.5:\n// astro:* event names → zfb:*\n// data-astro-prefetch → data-zfb-prefetch\n// __PREFETCH_DISABLED__ → <meta name=\"zfb-prefetch-disabled\" content=\"true\">\n//\n// DISABLED-FLAG CONTRACT — exact meta tag name and content value are locked:\n// meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]\n// Both this module and sibling sub-issue #272-config pin this verbatim.\n\nexport type PrefetchStrategy = \"hover\" | \"viewport\" | \"load\" | \"tap\";\n\nexport interface PrefetchInitOptions {\n prefetchAll?: boolean;\n defaultStrategy?: PrefetchStrategy;\n}\n\nexport interface PrefetchOptions {\n ignoreSlowConnection?: boolean;\n with?: \"link\" | \"fetch\";\n}\n\n// Module-level state — persists across SPA navigations within a session.\nlet initialized = false;\nlet prefetchAll = false;\nlet defaultStrategy: PrefetchStrategy = \"hover\";\n\n// Set of hrefs that have been successfully prefetched.\nconst prefetched = new Set<string>();\n// Map of in-flight prefetch promises for dedup (also used as the concurrent short-circuit guard).\nconst inFlight = new Map<string, Promise<void>>();\n\n// How long to wait for a <link rel=prefetch>'s load/error before treating\n// the prefetch as settled anyway (see executePrefetch).\nconst LINK_SETTLE_TIMEOUT_MS = 10_000;\n\n// The viewport observer — retained across post-swap re-scans so new elements\n// can be observed without recreating the observer.\nlet viewportObserver: IntersectionObserver | null = null;\n\n// Hover cancel handle — set when a pointerenter idle-callback fires, cleared on\n// pointerleave before the callback executes.\nconst hoverCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n// Focus cancel handle — set when a focusin idle-callback fires, cleared on\n// focusout before the callback executes.\nconst focusCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n/**\n * Reset all module-level state. Exported for test isolation only — not part of\n * the public API and not re-exported from any barrel.\n */\nexport function __resetForTests(): void {\n initialized = false;\n prefetchAll = false;\n defaultStrategy = \"hover\";\n prefetched.clear();\n inFlight.clear();\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n hoverCancelHandles.clear();\n focusCancelHandles.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Feature detection\n// ---------------------------------------------------------------------------\n\nfunction supportsLinkPrefetch(): boolean {\n const link = document.createElement(\"link\");\n return link.relList?.supports?.(\"prefetch\") ?? false;\n}\n\nfunction isSlowConnection(): boolean {\n const nav = navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n };\n return (\n nav.connection?.saveData === true ||\n nav.connection?.effectiveType === \"2g\" ||\n nav.connection?.effectiveType === \"slow-2g\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// Core prefetch logic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve an href to an absolute URL string keyed against the current origin.\n * Returns null for cross-origin hrefs (these are skipped entirely).\n */\nfunction resolveHref(href: string): string | null {\n try {\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) return null;\n return url.href;\n } catch {\n return null;\n }\n}\n\n/**\n * Execute a single prefetch for the given absolute href.\n * Uses <link rel=\"prefetch\"> (preferred) or fetch() fallback.\n */\nfunction executePrefetch(href: string, opts: PrefetchOptions): Promise<void> {\n const existing = inFlight.get(href);\n if (existing) return existing;\n\n const p: Promise<void> = (async () => {\n try {\n const method = opts.with ?? (supportsLinkPrefetch() ? \"link\" : \"fetch\");\n if (method === \"link\") {\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = href;\n // Wait for load/error so we only mark success on actual load and allow\n // retry after error — inserting without waiting would mark success\n // immediately even on failure. (Bug: Takazudo/zudo-front-builder#896)\n // Some browsers (e.g. Safari) never fire load/error on rel=prefetch\n // links; without the settle timeout the href would sit in inFlight\n // forever and block every future retry. Timing out counts as\n // success — the resource was requested, which is all prefetch needs.\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => resolve(), LINK_SETTLE_TIMEOUT_MS);\n link.addEventListener(\n \"load\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n link.addEventListener(\n \"error\",\n () => {\n clearTimeout(timer);\n // Remove the failed element so retries don't accumulate\n // dead <link> nodes in <head>.\n link.remove();\n reject(new Error(`prefetch link error: ${href}`));\n },\n { once: true },\n );\n document.head.appendChild(link);\n });\n } else {\n await fetch(href, { priority: \"low\" } as RequestInit & { priority?: string });\n }\n prefetched.add(href);\n } finally {\n inFlight.delete(href);\n }\n })();\n\n inFlight.set(href, p);\n return p;\n}\n\n/**\n * Public prefetch function. Idempotent per href.\n */\nexport function prefetch(url: string, opts: PrefetchOptions = {}): void {\n const href = resolveHref(url);\n if (!href) return; // cross-origin — skip\n\n if (opts.ignoreSlowConnection !== true && isSlowConnection()) return;\n\n if (prefetched.has(href) || inFlight.has(href)) return;\n\n executePrefetch(href, opts).catch(() => {});\n}\n\n// ---------------------------------------------------------------------------\n// Shared enter/leave handler factory\n// ---------------------------------------------------------------------------\n\ntype CancelHandleMap = Map<Element, ReturnType<typeof setTimeout> | number>;\n\n/**\n * Returns a pair of [enterHandler, leaveHandler] that queue and cancel an idle\n * prefetch callback, keyed on the supplied per-trigger cancel-handle map.\n *\n * Both the pointer (hover) and focus triggers use identical queue/cancel logic;\n * the only difference between them is which map tracks their pending handles.\n * Parameterising by the map eliminates that duplication.\n */\n// pointerenter/pointerleave (and focusin/focusout) fire on every element the\n// pointer/focus enters or leaves, including nested descendants — not just the\n// delegated document listener's eventual `closest(\"a[href]\")` target. For\n// `<a><span>text</span></a>`, moving the pointer between the link's own\n// children fires a leave on the child being left and an enter on the child\n// being entered, even though the pointer never left the LINK itself.\n// `relatedTarget` is where the pointer/focus is going (leave) or came from\n// (enter); reading it lets the two handlers below tell an intra-link move\n// apart from a real leave/first-enter. #1398.\ntype RelatedTargetEvent = Event & { relatedTarget?: EventTarget | null };\n\nfunction makeEnterLeaveHandlers(\n cancelHandles: CancelHandleMap,\n): [enterHandler: (e: Event) => void, leaveHandler: (e: Event) => void] {\n function enterHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"hover\")) return;\n\n // A handle is already queued for this link — the pointer/focus moved\n // between nested descendants of the SAME link, which re-fires enter on\n // each one. Keep the original debounce window instead of overwriting it\n // with a fresh requestIdleCallback/timeout that a busy pointer could keep\n // resetting indefinitely. #1398.\n if (cancelHandles.has(link)) return;\n\n // The pending debounce already FIRED for this link — its idle callback ran\n // prefetch(link.href), so the handle is gone from cancelHandles but the\n // href is now prefetched (or in-flight). A later enter on the SAME link\n // (e.g. an intra-link child->child move whose first callback fired in the\n // gap between the two pointer moves — which the synchronous-dispatch L2\n // suite cannot reproduce but a real browser does) must NOT queue a second,\n // redundant idle callback whose prefetch() would only no-op. This is the\n // fire+requeue companion to the cancel+requeue guard above. A failed\n // prefetch leaves the href in NEITHER set (executePrefetch deletes it from\n // inFlight without adding it to prefetched), so retry-after-error still\n // works. #1398.\n const resolvedHref = resolveHref(link.href);\n if (resolvedHref && (prefetched.has(resolvedHref) || inFlight.has(resolvedHref))) return;\n\n // Use requestIdleCallback if available, else a small timeout.\n const fire = () => {\n cancelHandles.delete(link);\n prefetch(link.href);\n };\n\n if (typeof requestIdleCallback !== \"undefined\") {\n const handle = requestIdleCallback(fire);\n cancelHandles.set(link, handle);\n } else {\n const handle = setTimeout(fire, 100);\n cancelHandles.set(link, handle);\n }\n }\n\n function leaveHandler(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n\n // The pointer/focus is moving to relatedTarget. If that's still inside\n // the same link (a nested descendant), this is NOT a real leave — skip\n // the cancel so the pending prefetch survives intra-link moves. #1398.\n const related = (e as RelatedTargetEvent).relatedTarget;\n if (related instanceof Node && link.contains(related)) return;\n\n const handle = cancelHandles.get(link);\n if (handle === undefined) return;\n\n if (typeof cancelIdleCallback !== \"undefined\") {\n cancelIdleCallback(handle as number);\n } else {\n clearTimeout(handle as ReturnType<typeof setTimeout>);\n }\n cancelHandles.delete(link);\n }\n\n return [enterHandler, leaveHandler];\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: hover (pointerenter / pointerleave)\n// ---------------------------------------------------------------------------\n\nconst [onPointerEnter, onPointerLeave] = makeEnterLeaveHandlers(hoverCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: focus (focusin / focusout with cancel on focusout)\n// ---------------------------------------------------------------------------\n\nconst [onFocusIn, onFocusOut] = makeEnterLeaveHandlers(focusCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: tap (touchstart / mousedown)\n// ---------------------------------------------------------------------------\n\nfunction onTap(e: Event): void {\n const link = (e.target as Element).closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!link) return;\n if (!shouldPrefetchLink(link, \"tap\")) return;\n prefetch(link.href);\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: viewport (IntersectionObserver)\n// ---------------------------------------------------------------------------\n\nfunction initViewportObserver(): IntersectionObserver {\n if (viewportObserver) return viewportObserver;\n\n viewportObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const link = entry.target as HTMLAnchorElement;\n prefetch(link.href);\n // Once observed and in-flight, no need to keep observing.\n viewportObserver?.unobserve(link);\n }\n }\n },\n { threshold: 0 },\n );\n\n return viewportObserver;\n}\n\nfunction observeViewportLinks(): void {\n const observer = initViewportObserver();\n const selector =\n prefetchAll && defaultStrategy === \"viewport\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='viewport']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (href && !prefetched.has(href)) {\n observer.observe(link);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: load (requestIdleCallback after DOMContentLoaded)\n// ---------------------------------------------------------------------------\n\nfunction prefetchLoadLinks(): void {\n const selector =\n prefetchAll && defaultStrategy === \"load\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='load']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (!href || prefetched.has(href)) return;\n\n if (typeof requestIdleCallback !== \"undefined\") {\n requestIdleCallback(() => prefetch(link.href));\n } else {\n setTimeout(() => prefetch(link.href), 0);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Per-link strategy resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Determine whether a given link should be prefetched for the supplied trigger strategy.\n * Returns false if:\n * - data-zfb-prefetch=\"false\" (always disabled)\n * - The effective strategy for this link doesn't match the active trigger\n */\nfunction shouldPrefetchLink(link: HTMLAnchorElement, triggerStrategy: PrefetchStrategy): boolean {\n const attr = link.dataset[\"zfbPrefetch\"];\n\n // Explicitly disabled.\n if (attr === \"false\") return false;\n\n // Per-link explicit strategy.\n if (attr && attr !== \"false\") {\n return attr === triggerStrategy;\n }\n\n // No explicit attribute — rely on prefetchAll + defaultStrategy.\n if (!prefetchAll) return false;\n\n return defaultStrategy === triggerStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Post-swap re-scan\n// ---------------------------------------------------------------------------\n\nfunction onAfterSwap(): void {\n // Disconnect the existing observer before re-scanning so detached anchors from\n // the old body are unobserved and don't accumulate across SPA swaps.\n // (Bug: Takazudo/zudo-front-builder#896 — observer leak across SPA swaps)\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n observeViewportLinks();\n prefetchLoadLinks();\n}\n\n// ---------------------------------------------------------------------------\n// init()\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the prefetch module.\n *\n * Idempotent — multiple calls are safe. The module-level `initialized` flag\n * ensures listeners are registered exactly once.\n *\n * DISABLED-FLAG CONTRACT: if `<meta name=\"zfb-prefetch-disabled\" content=\"true\">`\n * is present in the document at init() time, this function is a no-op.\n */\nexport function init(options?: PrefetchInitOptions): void {\n // Disabled-flag check — exact selector is the contract with #272-config.\n if (document.querySelector('meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]')) {\n return;\n }\n\n if (initialized) return;\n initialized = true;\n\n prefetchAll = options?.prefetchAll ?? false;\n defaultStrategy = options?.defaultStrategy ?? \"hover\";\n\n // Hover + tap — document delegation, picks up SPA-inserted links automatically.\n document.addEventListener(\"pointerenter\", onPointerEnter, { capture: true });\n document.addEventListener(\"pointerleave\", onPointerLeave, { capture: true });\n // Focus — separate cancel map so hover and focus don't share handles.\n document.addEventListener(\"focusin\", onFocusIn, { capture: true });\n document.addEventListener(\"focusout\", onFocusOut, { capture: true });\n document.addEventListener(\"touchstart\", onTap, { capture: true, passive: true });\n document.addEventListener(\"mousedown\", onTap, { capture: true });\n\n // Viewport — observe current links immediately.\n observeViewportLinks();\n\n // Load — queue current links via idle callback.\n const queueLoad = () => prefetchLoadLinks();\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", queueLoad, { once: true });\n } else {\n queueLoad();\n }\n\n // Post-swap re-scan — re-walk viewport + load links after each SPA navigation.\n document.addEventListener(\"zfb:after-swap\", onAfterSwap);\n}\n"]}
1
+ {"version":3,"file":"prefetch.js","sourceRoot":"","sources":["../../src/client-router/prefetch.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference lib=\"dom\" />\n/// <reference lib=\"dom.iterable\" />\n// `@takazudo/zfb-runtime` — prefetch module.\n//\n// Ported from Astro's prefetch module (packages/astro/src/prefetch/index.ts).\n// Source: https://github.com/withastro/astro\n// Issue: Takazudo/zudo-front-builder#276\n//\n// Mechanical renames per W1B §13.5:\n// astro:* event names → zfb:*\n// data-astro-prefetch → data-zfb-prefetch\n// __PREFETCH_DISABLED__ → <meta name=\"zfb-prefetch-disabled\" content=\"true\">\n//\n// DISABLED-FLAG CONTRACT — exact meta tag name and content value are locked:\n// meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]\n// Both this module and sibling sub-issue #272-config pin this verbatim.\n\nexport type PrefetchStrategy = \"hover\" | \"viewport\" | \"load\" | \"tap\";\n\nexport interface PrefetchInitOptions {\n prefetchAll?: boolean;\n defaultStrategy?: PrefetchStrategy;\n}\n\nexport interface PrefetchOptions {\n ignoreSlowConnection?: boolean;\n with?: \"link\" | \"fetch\";\n}\n\n// Module-level state — persists across SPA navigations within a session.\nlet initialized = false;\nlet prefetchAll = false;\nlet defaultStrategy: PrefetchStrategy = \"hover\";\n\n// Set of hrefs that have been successfully prefetched.\nconst prefetched = new Set<string>();\n// Map of in-flight prefetch promises for dedup (also used as the concurrent short-circuit guard).\nconst inFlight = new Map<string, Promise<void>>();\n\n// How long to wait for a <link rel=prefetch>'s load/error before treating\n// the prefetch as settled anyway (see executePrefetch).\nconst LINK_SETTLE_TIMEOUT_MS = 10_000;\n\n// The viewport observer — retained across post-swap re-scans so new elements\n// can be observed without recreating the observer.\nlet viewportObserver: IntersectionObserver | null = null;\n\n// Hover cancel handle — set when a pointerenter idle-callback fires, cleared on\n// pointerleave before the callback executes.\nconst hoverCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n// Focus cancel handle — set when a focusin idle-callback fires, cleared on\n// focusout before the callback executes.\nconst focusCancelHandles = new Map<Element, ReturnType<typeof setTimeout> | number>();\n\n/**\n * Reset all module-level state. Exported for test isolation only — not part of\n * the public API and not re-exported from any barrel.\n */\nexport function __resetForTests(): void {\n initialized = false;\n prefetchAll = false;\n defaultStrategy = \"hover\";\n prefetched.clear();\n inFlight.clear();\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n hoverCancelHandles.clear();\n focusCancelHandles.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Feature detection\n// ---------------------------------------------------------------------------\n\nfunction supportsLinkPrefetch(): boolean {\n const link = document.createElement(\"link\");\n return link.relList?.supports?.(\"prefetch\") ?? false;\n}\n\nfunction isSlowConnection(): boolean {\n const nav = navigator as Navigator & {\n connection?: { saveData?: boolean; effectiveType?: string };\n };\n return (\n nav.connection?.saveData === true ||\n nav.connection?.effectiveType === \"2g\" ||\n nav.connection?.effectiveType === \"slow-2g\"\n );\n}\n\n// ---------------------------------------------------------------------------\n// Core prefetch logic\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve an href to an absolute URL string keyed against the current origin.\n * Returns null for cross-origin hrefs (these are skipped entirely).\n */\nfunction resolveHref(href: string): string | null {\n try {\n const url = new URL(href, location.href);\n if (url.origin !== location.origin) return null;\n return url.href;\n } catch {\n return null;\n }\n}\n\n/**\n * Execute a single prefetch for the given absolute href.\n * Uses <link rel=\"prefetch\"> (preferred) or fetch() fallback.\n */\nfunction executePrefetch(href: string, opts: PrefetchOptions): Promise<void> {\n const existing = inFlight.get(href);\n if (existing) return existing;\n\n const p: Promise<void> = (async () => {\n try {\n const method = opts.with ?? (supportsLinkPrefetch() ? \"link\" : \"fetch\");\n if (method === \"link\") {\n const link = document.createElement(\"link\");\n link.rel = \"prefetch\";\n link.href = href;\n // Wait for load/error so we only mark success on actual load and allow\n // retry after error — inserting without waiting would mark success\n // immediately even on failure. (Bug: Takazudo/zudo-front-builder#896)\n // Some browsers (e.g. Safari) never fire load/error on rel=prefetch\n // links; without the settle timeout the href would sit in inFlight\n // forever and block every future retry. Timing out counts as\n // success — the resource was requested, which is all prefetch needs.\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => resolve(), LINK_SETTLE_TIMEOUT_MS);\n link.addEventListener(\n \"load\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n link.addEventListener(\n \"error\",\n () => {\n clearTimeout(timer);\n // Remove the failed element so retries don't accumulate\n // dead <link> nodes in <head>.\n link.remove();\n reject(new Error(`prefetch link error: ${href}`));\n },\n { once: true },\n );\n document.head.appendChild(link);\n });\n } else {\n await fetch(href, { priority: \"low\" } as RequestInit & { priority?: string });\n }\n prefetched.add(href);\n } finally {\n inFlight.delete(href);\n }\n })();\n\n inFlight.set(href, p);\n return p;\n}\n\n/**\n * Public prefetch function. Idempotent per href.\n */\nexport function prefetch(url: string, opts: PrefetchOptions = {}): void {\n const href = resolveHref(url);\n if (!href) return; // cross-origin — skip\n\n if (opts.ignoreSlowConnection !== true && isSlowConnection()) return;\n\n if (prefetched.has(href) || inFlight.has(href)) return;\n\n executePrefetch(href, opts).catch(() => {});\n}\n\n// ---------------------------------------------------------------------------\n// Shared enter/leave handler factory\n// ---------------------------------------------------------------------------\n\ntype CancelHandleMap = Map<Element, ReturnType<typeof setTimeout> | number>;\n\n/**\n * Returns a pair of [enterHandler, leaveHandler] that queue and cancel an idle\n * prefetch callback, keyed on the supplied per-trigger cancel-handle map.\n *\n * Both the pointer (hover) and focus triggers use identical queue/cancel logic;\n * the only difference between them is which map tracks their pending handles.\n * Parameterising by the map eliminates that duplication.\n */\n// pointerenter/pointerleave (and focusin/focusout) fire on every element the\n// pointer/focus enters or leaves, including nested descendants — not just the\n// delegated document listener's eventual `closest(\"a[href]\")` target. For\n// `<a><span>text</span></a>`, moving the pointer between the link's own\n// children fires a leave on the child being left and an enter on the child\n// being entered, even though the pointer never left the LINK itself.\n// `relatedTarget` is where the pointer/focus is going (leave) or came from\n// (enter); reading it lets the two handlers below tell an intra-link move\n// apart from a real leave/first-enter. #1398.\ntype RelatedTargetEvent = Event & { relatedTarget?: EventTarget | null };\n\n// Capture listeners also receive events targeted at document or text nodes.\nfunction closestLink(target: EventTarget | null): HTMLAnchorElement | null {\n return target instanceof Element ? target.closest<HTMLAnchorElement>(\"a[href]\") : null;\n}\n\nfunction makeEnterLeaveHandlers(\n cancelHandles: CancelHandleMap,\n): [enterHandler: (e: Event) => void, leaveHandler: (e: Event) => void] {\n function enterHandler(e: Event): void {\n const link = closestLink(e.target);\n if (!link) return;\n if (!shouldPrefetchLink(link, \"hover\")) return;\n\n // A handle is already queued for this link — the pointer/focus moved\n // between nested descendants of the SAME link, which re-fires enter on\n // each one. Keep the original debounce window instead of overwriting it\n // with a fresh requestIdleCallback/timeout that a busy pointer could keep\n // resetting indefinitely. #1398.\n if (cancelHandles.has(link)) return;\n\n // The pending debounce already FIRED for this link — its idle callback ran\n // prefetch(link.href), so the handle is gone from cancelHandles but the\n // href is now prefetched (or in-flight). A later enter on the SAME link\n // (e.g. an intra-link child->child move whose first callback fired in the\n // gap between the two pointer moves — which the synchronous-dispatch L2\n // suite cannot reproduce but a real browser does) must NOT queue a second,\n // redundant idle callback whose prefetch() would only no-op. This is the\n // fire+requeue companion to the cancel+requeue guard above. A failed\n // prefetch leaves the href in NEITHER set (executePrefetch deletes it from\n // inFlight without adding it to prefetched), so retry-after-error still\n // works. #1398.\n const resolvedHref = resolveHref(link.href);\n if (resolvedHref && (prefetched.has(resolvedHref) || inFlight.has(resolvedHref))) return;\n\n // Use requestIdleCallback if available, else a small timeout.\n const fire = () => {\n cancelHandles.delete(link);\n prefetch(link.href);\n };\n\n if (typeof requestIdleCallback !== \"undefined\") {\n const handle = requestIdleCallback(fire);\n cancelHandles.set(link, handle);\n } else {\n const handle = setTimeout(fire, 100);\n cancelHandles.set(link, handle);\n }\n }\n\n function leaveHandler(e: Event): void {\n const link = closestLink(e.target);\n if (!link) return;\n\n // The pointer/focus is moving to relatedTarget. If that's still inside\n // the same link (a nested descendant), this is NOT a real leave — skip\n // the cancel so the pending prefetch survives intra-link moves. #1398.\n const related = (e as RelatedTargetEvent).relatedTarget;\n if (related instanceof Node && link.contains(related)) return;\n\n const handle = cancelHandles.get(link);\n if (handle === undefined) return;\n\n if (typeof cancelIdleCallback !== \"undefined\") {\n cancelIdleCallback(handle as number);\n } else {\n clearTimeout(handle as ReturnType<typeof setTimeout>);\n }\n cancelHandles.delete(link);\n }\n\n return [enterHandler, leaveHandler];\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: hover (pointerenter / pointerleave)\n// ---------------------------------------------------------------------------\n\nconst [onPointerEnter, onPointerLeave] = makeEnterLeaveHandlers(hoverCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: focus (focusin / focusout with cancel on focusout)\n// ---------------------------------------------------------------------------\n\nconst [onFocusIn, onFocusOut] = makeEnterLeaveHandlers(focusCancelHandles);\n\n// ---------------------------------------------------------------------------\n// Trigger: tap (touchstart / mousedown)\n// ---------------------------------------------------------------------------\n\nfunction onTap(e: Event): void {\n const link = closestLink(e.target);\n if (!link) return;\n if (!shouldPrefetchLink(link, \"tap\")) return;\n prefetch(link.href);\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: viewport (IntersectionObserver)\n// ---------------------------------------------------------------------------\n\nfunction initViewportObserver(): IntersectionObserver {\n if (viewportObserver) return viewportObserver;\n\n viewportObserver = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n const link = entry.target as HTMLAnchorElement;\n prefetch(link.href);\n // Once observed and in-flight, no need to keep observing.\n viewportObserver?.unobserve(link);\n }\n }\n },\n { threshold: 0 },\n );\n\n return viewportObserver;\n}\n\nfunction observeViewportLinks(): void {\n const observer = initViewportObserver();\n const selector =\n prefetchAll && defaultStrategy === \"viewport\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='viewport']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (href && !prefetched.has(href)) {\n observer.observe(link);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Trigger: load (requestIdleCallback after DOMContentLoaded)\n// ---------------------------------------------------------------------------\n\nfunction prefetchLoadLinks(): void {\n const selector =\n prefetchAll && defaultStrategy === \"load\"\n ? \"a[href]:not([data-zfb-prefetch='false'])\"\n : \"a[data-zfb-prefetch='load']\";\n\n document.querySelectorAll<HTMLAnchorElement>(selector).forEach((link) => {\n const href = resolveHref(link.href);\n if (!href || prefetched.has(href)) return;\n\n if (typeof requestIdleCallback !== \"undefined\") {\n requestIdleCallback(() => prefetch(link.href));\n } else {\n setTimeout(() => prefetch(link.href), 0);\n }\n });\n}\n\n// ---------------------------------------------------------------------------\n// Per-link strategy resolution\n// ---------------------------------------------------------------------------\n\n/**\n * Determine whether a given link should be prefetched for the supplied trigger strategy.\n * Returns false if:\n * - data-zfb-prefetch=\"false\" (always disabled)\n * - The effective strategy for this link doesn't match the active trigger\n */\nfunction shouldPrefetchLink(link: HTMLAnchorElement, triggerStrategy: PrefetchStrategy): boolean {\n const attr = link.dataset[\"zfbPrefetch\"];\n\n // Explicitly disabled.\n if (attr === \"false\") return false;\n\n // Per-link explicit strategy.\n if (attr && attr !== \"false\") {\n return attr === triggerStrategy;\n }\n\n // No explicit attribute — rely on prefetchAll + defaultStrategy.\n if (!prefetchAll) return false;\n\n return defaultStrategy === triggerStrategy;\n}\n\n// ---------------------------------------------------------------------------\n// Post-swap re-scan\n// ---------------------------------------------------------------------------\n\nfunction onAfterSwap(): void {\n // Disconnect the existing observer before re-scanning so detached anchors from\n // the old body are unobserved and don't accumulate across SPA swaps.\n // (Bug: Takazudo/zudo-front-builder#896 — observer leak across SPA swaps)\n if (viewportObserver) {\n viewportObserver.disconnect();\n viewportObserver = null;\n }\n observeViewportLinks();\n prefetchLoadLinks();\n}\n\n// ---------------------------------------------------------------------------\n// init()\n// ---------------------------------------------------------------------------\n\n/**\n * Initialize the prefetch module.\n *\n * Idempotent — multiple calls are safe. The module-level `initialized` flag\n * ensures listeners are registered exactly once.\n *\n * DISABLED-FLAG CONTRACT: if `<meta name=\"zfb-prefetch-disabled\" content=\"true\">`\n * is present in the document at init() time, this function is a no-op.\n */\nexport function init(options?: PrefetchInitOptions): void {\n // Disabled-flag check — exact selector is the contract with #272-config.\n if (document.querySelector('meta[name=\"zfb-prefetch-disabled\"][content=\"true\"]')) {\n return;\n }\n\n if (initialized) return;\n initialized = true;\n\n prefetchAll = options?.prefetchAll ?? false;\n defaultStrategy = options?.defaultStrategy ?? \"hover\";\n\n // Hover + tap — document delegation, picks up SPA-inserted links automatically.\n document.addEventListener(\"pointerenter\", onPointerEnter, { capture: true });\n document.addEventListener(\"pointerleave\", onPointerLeave, { capture: true });\n // Focus — separate cancel map so hover and focus don't share handles.\n document.addEventListener(\"focusin\", onFocusIn, { capture: true });\n document.addEventListener(\"focusout\", onFocusOut, { capture: true });\n document.addEventListener(\"touchstart\", onTap, { capture: true, passive: true });\n document.addEventListener(\"mousedown\", onTap, { capture: true });\n\n // Viewport — observe current links immediately.\n observeViewportLinks();\n\n // Load — queue current links via idle callback.\n const queueLoad = () => prefetchLoadLinks();\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", queueLoad, { once: true });\n } else {\n queueLoad();\n }\n\n // Post-swap re-scan — re-walk viewport + load links after each SPA navigation.\n document.addEventListener(\"zfb:after-swap\", onAfterSwap);\n}\n"]}
@@ -53,7 +53,7 @@
53
53
  // missing/NaN `state.index` that a bfcache restore can leave behind (#1076).
54
54
  import { doPreparation, doSwap, onPageLoad, triggerEvent, updateScrollPosition, } from "./events.js";
55
55
  import { safePushState, safeReplaceState } from "./history-safe.js";
56
- import { detectScriptExecuted } from "./swap-functions.js";
56
+ import { assertUniquePersistKeys, detectScriptExecuted } from "./swap-functions.js";
57
57
  // Island re-bootstrap and deferred-cancel after body swap (W1B §12.2, §12.5).
58
58
  // mountNewIslands() is called after runScripts() and before onPageLoad().
59
59
  // cancelPendingIslands() and unmountIslands() run inside doSwap()'s synchronous
@@ -509,6 +509,8 @@ historyCommittedEarly = false) {
509
509
  // Teardown and swap are a synchronous point-of-no-return. doSwap checks
510
510
  // the signal after the animation await and invokes this callback only for
511
511
  // a navigation that still owns the commit.
512
+ assertUniquePersistKeys(document, "current");
513
+ assertUniquePersistKeys(event.newDocument, "incoming");
512
514
  currentNavigation.domCommitStarted = true;
513
515
  cancelPendingIslands();
514
516
  // Unmount mounted islands on the OLD body before the swap so component
@@ -765,6 +767,10 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
765
767
  currentTransition.viewTransition = document.startViewTransition(async () => {
766
768
  domUpdateOutcome = await updateDOM(prepEvent, options, currentNavigation, currentTransition, historyState, undefined, historyCommittedEarly);
767
769
  });
770
+ // A skipped native transition rejects ready even when its update callback
771
+ // succeeds (or never runs). The browser does not mark ready as handled.
772
+ // Update failures are reported through updateCallbackDone below.
773
+ currentTransition.viewTransition.ready.catch(() => { });
768
774
  }
769
775
  else {
770
776
  // Simulation mode requires a bit more manual work.
@@ -804,7 +810,7 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
804
810
  }
805
811
  // In earlier versions was then'ed on viewTransition.ready which would not execute
806
812
  // if the visual part of the transition has errors or was skipped
807
- currentTransition.viewTransition?.updateCallbackDone.finally(async () => {
813
+ currentTransition.viewTransition?.updateCallbackDone.then(async () => {
808
814
  if (domUpdateOutcome !== "swapped")
809
815
  return;
810
816
  await runScripts();
@@ -815,10 +821,10 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
815
821
  mountNewIslands();
816
822
  onPageLoad();
817
823
  announce();
818
- });
824
+ }, () => { });
819
825
  // finished.ready and finished.finally are the same for the simulation but not
820
826
  // necessarily for native view transition, where finished rejects when updateCallbackDone does.
821
- currentTransition.viewTransition?.finished.finally(() => {
827
+ const clearTransition = () => {
822
828
  // exactOptionalPropertyTypes: true forbids assigning `undefined` to an optional
823
829
  // `viewTransition?: ViewTransition` slot — `delete` is the equivalent reset.
824
830
  delete currentTransition.viewTransition;
@@ -828,7 +834,9 @@ async function transition(direction, from, to, options, historyState, hasUAVisua
828
834
  mostRecentNavigation = undefined;
829
835
  document.documentElement.removeAttribute(DIRECTION_ATTR);
830
836
  document.documentElement.removeAttribute(OLD_NEW_ATTR);
831
- });
837
+ };
838
+ // A discarded finally() child would itself reject on an update error.
839
+ currentTransition.viewTransition?.finished.then(clearTransition, clearTransition);
832
840
  try {
833
841
  // Compatibility:
834
842
  // In an earlier version we awaited viewTransition.ready, which includes animation setup.