From 57d6d216db484476528ae3e6278a0275f1105fc5 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Tue, 30 Jan 2024 22:31:51 -0500 Subject: [PATCH] `LinkPrefetchObserver`: listen for complementary events Prior to this commit, the `LinkPrefetchObserver` only listened for `mouseleave` events to clear the `PrefetchCache` instance. Not only were `mouseenter` events excluded, but the `mouseleave` event listeners were attached directly to the `` element with a `{ once: true }` option. While unlikely, its was for those event listeners to never be removed if a `mouseleave` were to not fire. Similarly, during `touchstart` events the event listener were added, but never removed since there wasn't a a complementary `mouseleave` event firing to remove it. This commit makes two changes to the event listeners: 1. extract the `addEventListener` calls to a loop, looping over `mouseenter` and `touchstart` event names 2. define complementary events for both `mouseenter` and `touchstart` By moving the cancellation logic out of individual event listeners and into an `this.eventTarget`-wide scope, we limit the risk of leaking listeners. Similarly, we only ever instantiate one per event-pairing. To track the `` element reference, define both a `this.#tryToCancelPrefetchRequest` method and a `this.#linkToPrefetch` property to hold the reference to the `` element in question. --- src/observers/link_prefetch_observer.js | 51 ++++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/observers/link_prefetch_observer.js b/src/observers/link_prefetch_observer.js index 165569dbd..99231e677 100644 --- a/src/observers/link_prefetch_observer.js +++ b/src/observers/link_prefetch_observer.js @@ -9,10 +9,14 @@ import { StreamMessage } from "../core/streams/stream_message" import { FetchMethod, FetchRequest } from "../http/fetch_request" import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache" +const observedEvents = { + "mouseenter": "mouseleave", + "touchstart": "touchcancel" +} + export class LinkPrefetchObserver { started = false - hoverTriggerEvent = "mouseenter" - touchTriggerEvent = "touchstart" + #linkToPrefetch = null constructor(delegate, eventTarget) { this.delegate = delegate @@ -32,26 +36,30 @@ export class LinkPrefetchObserver { stop() { if (!this.started) return - this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true - }) - this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true + Object.entries(observedEvents).forEach(([startEventName, stopEventName]) => { + this.eventTarget.removeEventListener(startEventName, this.#tryToPrefetchRequest, { + capture: true, + passive: true + }) + this.eventTarget.removeEventListener(stopEventName, this.#tryToCancelPrefetchRequest, { + capture: true, + passive: true + }) }) this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = false } #enable = () => { - this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true - }) - this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, { - capture: true, - passive: true + Object.entries(observedEvents).forEach(([startEventName, stopEventName]) => { + this.eventTarget.addEventListener(startEventName, this.#tryToPrefetchRequest, { + capture: true, + passive: true + }) + this.eventTarget.addEventListener(stopEventName, this.#tryToCancelPrefetchRequest, { + capture: true, + passive: true + }) }) this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true) this.started = true @@ -68,6 +76,7 @@ export class LinkPrefetchObserver { const location = getLocationForLink(link) if (this.delegate.canPrefetchRequestToLocation(link, location)) { + this.#linkToPrefetch = link const fetchRequest = new FetchRequest( this, FetchMethod.get, @@ -77,12 +86,18 @@ export class LinkPrefetchObserver { ) prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl) - - link.addEventListener("mouseleave", () => prefetchCache.clear(), { once: true }) } } } + #tryToCancelPrefetchRequest = (event) => { + if (event.target === this.#linkToPrefetch) { + prefetchCache.clear() + } + + this.#linkToPrefetch = null + } + #tryToUsePrefetchedRequest = (event) => { if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") { const cached = prefetchCache.get(event.detail.url.toString())