Skip to content

Commit

Permalink
LinkPrefetchObserver: listen for complementary events
Browse files Browse the repository at this point in the history
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 `<a>` 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 `<a>` element reference, define both a
`this.#tryToCancelPrefetchRequest` method and a `this.#linkToPrefetch`
property to hold the reference to the `<a>` element in question.
  • Loading branch information
seanpdoyle committed Jan 31, 2024
1 parent f81f9c2 commit 2efc325
Showing 1 changed file with 33 additions and 18 deletions.
51 changes: 33 additions & 18 deletions src/observers/link_prefetch_observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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())
Expand Down

0 comments on commit 2efc325

Please sign in to comment.