Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion src/observers/link_prefetch_observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { prefetchCache, cacheTtl } from "../core/drive/prefetch_cache"
export class LinkPrefetchObserver {
started = false
#prefetchedLink = null
#pendingFetchRequests = new Map()

constructor(delegate, eventTarget) {
this.delegate = delegate
Expand Down Expand Up @@ -40,6 +41,7 @@ export class LinkPrefetchObserver {
})

this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
this.eventTarget.removeEventListener("turbo:before-visit", this.#cancelPendingFetchRequests, true)
this.started = false
}

Expand All @@ -54,6 +56,7 @@ export class LinkPrefetchObserver {
})

this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true)
this.eventTarget.addEventListener("turbo:before-visit", this.#cancelPendingFetchRequests, true)
this.started = true
}

Expand All @@ -80,11 +83,29 @@ export class LinkPrefetchObserver {

fetchRequest.fetchOptions.priority = "low"

// Memorize fetch request and cancel previous one to the same URL
const url = location.href
const lastFetchRequest = this.#pendingFetchRequests.get(url)

if(lastFetchRequest) lastFetchRequest.cancel()
this.#pendingFetchRequests.set(url, fetchRequest)

prefetchCache.putLater(location, fetchRequest, this.#cacheTtl)
}
}
}

#cancelPendingFetchRequests = (event) => {
if (event.detail.fetchRequest) return

for (const [url, fetchRequest] of this.#pendingFetchRequests.entries()) {
if (url !== event.detail.url) {
fetchRequest.cancel()
this.#pendingFetchRequests.delete(url)
}
}
}

#cancelRequestIfObsolete = (event) => {
if (event.target === this.#prefetchedLink) this.#cancelPrefetchRequest()
}
Expand Down Expand Up @@ -128,7 +149,13 @@ export class LinkPrefetchObserver {

requestErrored(fetchRequest) {}

requestFinished(fetchRequest) {}
requestFinished(fetchRequest) {
const url = fetchRequest.url.href

if(this.#pendingFetchRequests.get(url) === fetchRequest) {
this.#pendingFetchRequests.delete(url)
}
}

requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}

Expand Down
1 change: 1 addition & 0 deletions src/tests/fixtures/hover_to_prefetch.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<body>
<a href="/src/tests/fixtures/prefetched.html" id="anchor_for_prefetch">Hover to prefetch me</a>
<a href="/src/tests/fixtures/bare.html" id="anchor_for_prefetch_other_href">Hover to prefetch me</a>
<a href="/__turbo/delayed_response" id="anchor_for_slow_prefetch">Hover to prefetch me (response, that takes long to render)</a>
<a href="/src/tests/fixtures/prefetched.html" id="anchor_with_inner_elements">
<span>Hover to prefetch me</span>
</a>
Expand Down
68 changes: 68 additions & 0 deletions src/tests/functional/link_prefetch_observer_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,74 @@ test("it cancels the prefetch request if the link is no longer hovered", async (
})
})

test("it cancels pending prefetch requests if a new request is made", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })

// Prefetch a request, that takes a long time to load
await hoverSelector({ page, selector: "#anchor_for_slow_prefetch" })
await page.click("#anchor_for_slow_prefetch", {noWaitAfter: true})

// Issue a new request to a secondary resource, that loads fast
await page.click("#anchor_for_prefetch")

// Allow for the slow request to be processed if it wasn't canceled
await sleep(1100)

// The page should show the secondary resource
await expect(page).toHaveTitle("Prefetched Page")
await expect(page).toHaveURL("src/tests/fixtures/prefetched.html")
})

test("it cancels pending prefetch requests if a new prefetch request is made", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })

// Prefetch a request, that takes a long time to load
await hoverSelector({ page, selector: "#anchor_for_slow_prefetch" })
await page.click("#anchor_for_slow_prefetch", {noWaitAfter: true})

// Issue a new request including prefetch to a secondary resource, that loads fast
await hoverSelector({ page, selector: "#anchor_for_prefetch" })
await page.click("#anchor_for_prefetch")

await sleep(1100)

// The page should show the secondary resource
await expect(page).toHaveTitle("Prefetched Page")
await expect(page).toHaveURL("src/tests/fixtures/prefetched.html")
})

test("it cancels the pending prefetch request if the same link is hovered again", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })

let finishedRequestCount = 0
page.on("requestfinished", async () => (finishedRequestCount++))

await page.hover("#anchor_for_slow_prefetch")
await sleep(150)
await page.mouse.move(0, 0)

// Prefetching the same URL again while the last request is still not finished should abort the latter
await page.hover("#anchor_for_slow_prefetch")

await sleep(1200)
expect(finishedRequestCount).toEqual(1)
})

test("it keeps the running prefetch request when clicking a link", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })

let requestCount = 0
page.on("request", async () => (requestCount++))

await hoverSelector({ page, selector: "#anchor_for_slow_prefetch" })
await sleep(150)
await page.click("#anchor_for_slow_prefetch")

// The prefetch request should be used for the visit
await expect(page).toHaveTitle("One")
expect(requestCount).toEqual(1)
})

test("it resets the cache when a link is hovered", async ({ page }) => {
await goTo({ page, path: "/hover_to_prefetch.html" })

Expand Down