diff --git a/src/core/cache.js b/src/core/cache.js index 580c6ab1f..0750a938c 100644 --- a/src/core/cache.js +++ b/src/core/cache.js @@ -1,9 +1,8 @@ import { setMetaContent } from "../util" -import { SnapshotCache } from "./drive/snapshot_cache" export class Cache { clear() { - this.store.clear() + this.session.clearCache() } resetCacheControl() { @@ -18,18 +17,6 @@ export class Cache { this.#setCacheControl("no-preview") } - set store(store) { - if (typeof store === "string") { - SnapshotCache.setStore(store) - } else { - SnapshotCache.currentStore = store - } - } - - get store() { - return SnapshotCache.currentStore - } - #setCacheControl(value) { setMetaContent("turbo-cache-control", value) } diff --git a/src/core/drive/cache_stores/disk_store.js b/src/core/drive/cache_stores/disk_store.js deleted file mode 100644 index 285caa5d9..000000000 --- a/src/core/drive/cache_stores/disk_store.js +++ /dev/null @@ -1,64 +0,0 @@ -import { PageSnapshot } from "../page_snapshot" - -export class DiskStore { - _version = "v1" - - constructor() { - if (typeof caches === "undefined") { - throw new Error("windows.caches is undefined. CacheStore requires a secure context.") - } - - this.storage = this.openStorage() - } - - async has(location) { - const storage = await this.openStorage() - return (await storage.match(location)) !== undefined - } - - async get(location) { - const storage = await this.openStorage() - const response = await storage.match(location) - - if (response && response.ok) { - const html = await response.text() - return PageSnapshot.fromHTMLString(html) - } - } - - async put(location, snapshot) { - const storage = await this.openStorage() - - const response = new Response(snapshot.html, { - status: 200, - statusText: "OK", - headers: { - "Content-Type": "text/html" - } - }) - await storage.put(location, response) - return snapshot - } - - async clear() { - const storage = await this.openStorage() - const keys = await storage.keys() - await Promise.all(keys.map((key) => storage.delete(key))) - } - - openStorage() { - this.storage ||= caches.open(`turbo-${this.version}`) - return this.storage - } - - set version(value) { - if (value !== this._version) { - this._version = value - this.storage ||= caches.open(`turbo-${this.version}`) - } - } - - get version() { - return this._version - } -} diff --git a/src/core/drive/cache_stores/memory_store.js b/src/core/drive/cache_stores/memory_store.js deleted file mode 100644 index 3ec8ae0b1..000000000 --- a/src/core/drive/cache_stores/memory_store.js +++ /dev/null @@ -1,56 +0,0 @@ -import { toCacheKey } from "../../url" - -export class MemoryStore { - keys = [] - snapshots = {} - - constructor(size) { - this.size = size - } - - async has(location) { - return toCacheKey(location) in this.snapshots - } - - async get(location) { - if (await this.has(location)) { - const snapshot = this.read(location) - this.touch(location) - return snapshot - } - } - - async put(location, snapshot) { - this.write(location, snapshot) - this.touch(location) - return snapshot - } - - async clear() { - this.snapshots = {} - } - - // Private - - read(location) { - return this.snapshots[toCacheKey(location)] - } - - write(location, snapshot) { - this.snapshots[toCacheKey(location)] = snapshot - } - - touch(location) { - const key = toCacheKey(location) - const index = this.keys.indexOf(key) - if (index > -1) this.keys.splice(index, 1) - this.keys.unshift(key) - this.trim() - } - - trim() { - for (const key of this.keys.splice(this.size)) { - delete this.snapshots[key] - } - } -} diff --git a/src/core/drive/navigator.js b/src/core/drive/navigator.js index 999d97817..54b01e8ee 100644 --- a/src/core/drive/navigator.js +++ b/src/core/drive/navigator.js @@ -21,7 +21,7 @@ export class Navigator { referrer: this.location, ...options }) - return this.currentVisit.start() + this.currentVisit.start() } submitForm(form, submitter) { diff --git a/src/core/drive/page_snapshot.js b/src/core/drive/page_snapshot.js index 774a4af94..58a5a75a9 100644 --- a/src/core/drive/page_snapshot.js +++ b/src/core/drive/page_snapshot.js @@ -45,10 +45,6 @@ export class PageSnapshot extends Snapshot { return this.documentElement.getAttribute("lang") } - get html() { - return `${this.headElement.outerHTML}\n\n${this.element.outerHTML}` - } - get headElement() { return this.headSnapshot.element } diff --git a/src/core/drive/page_view.js b/src/core/drive/page_view.js index 249b73efc..0508d0960 100644 --- a/src/core/drive/page_view.js +++ b/src/core/drive/page_view.js @@ -7,7 +7,7 @@ import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" export class PageView extends View { - snapshotCache = new SnapshotCache() + snapshotCache = new SnapshotCache(10) lastRenderedLocation = new URL(location.href) forceReloaded = false @@ -36,10 +36,6 @@ export class PageView extends View { return this.render(renderer) } - setCacheStore(cacheName) { - SnapshotCache.setStore(cacheName) - } - clearSnapshotCache() { this.snapshotCache.clear() } diff --git a/src/core/drive/preloader.js b/src/core/drive/preloader.js index 62e0825f6..ff9d871d6 100644 --- a/src/core/drive/preloader.js +++ b/src/core/drive/preloader.js @@ -31,7 +31,9 @@ export class Preloader { async preloadURL(link) { const location = new URL(link.href) - if (await this.snapshotCache.has(location)) return + if (this.snapshotCache.has(location)) { + return + } try { const response = await fetch(location.toString(), { headers: { "Sec-Purpose": "prefetch", Accept: "text/html" } }) diff --git a/src/core/drive/snapshot_cache.js b/src/core/drive/snapshot_cache.js index 8e8c53c02..6ed37e8fd 100644 --- a/src/core/drive/snapshot_cache.js +++ b/src/core/drive/snapshot_cache.js @@ -1,35 +1,56 @@ -import { DiskStore } from "./cache_stores/disk_store" -import { MemoryStore } from "./cache_stores/memory_store" +import { toCacheKey } from "../url" export class SnapshotCache { - static currentStore = new MemoryStore(10) - - static setStore(storeName) { - switch (storeName) { - case "memory": - SnapshotCache.currentStore = new MemoryStore(10) - break - case "disk": - SnapshotCache.currentStore = new DiskStore() - break - default: - throw new Error(`Invalid store name: ${storeName}`) - } + keys = [] + snapshots = {} + + constructor(size) { + this.size = size } has(location) { - return SnapshotCache.currentStore.has(location) + return toCacheKey(location) in this.snapshots } get(location) { - return SnapshotCache.currentStore.get(location) + if (this.has(location)) { + const snapshot = this.read(location) + this.touch(location) + return snapshot + } } put(location, snapshot) { - return SnapshotCache.currentStore.put(location, snapshot) + this.write(location, snapshot) + this.touch(location) + return snapshot } clear() { - return SnapshotCache.currentStore.clear() + this.snapshots = {} + } + + // Private + + read(location) { + return this.snapshots[toCacheKey(location)] + } + + write(location, snapshot) { + this.snapshots[toCacheKey(location)] = snapshot + } + + touch(location) { + const key = toCacheKey(location) + const index = this.keys.indexOf(key) + if (index > -1) this.keys.splice(index, 1) + this.keys.unshift(key) + this.trim() + } + + trim() { + for (const key of this.keys.splice(this.size)) { + delete this.snapshots[key] + } } } diff --git a/src/core/drive/visit.js b/src/core/drive/visit.js index ce31fee7e..4e7796302 100644 --- a/src/core/drive/visit.js +++ b/src/core/drive/visit.js @@ -105,11 +105,10 @@ export class Visit { return this.isSamePage } - async start() { + start() { if (this.state == VisitState.initialized) { this.recordTimingMetric(TimingMetric.visitStart) this.state = VisitState.started - this.cachedSnapshot = await this.getCachedSnapshot() this.adapter.visitStarted(this) this.delegate.visitStarted(this) } @@ -155,10 +154,10 @@ export class Visit { } } - async issueRequest() { + issueRequest() { if (this.hasPreloadedResponse()) { this.simulateRequest() - } else if (!this.request && await this.shouldIssueRequest()) { + } else if (this.shouldIssueRequest() && !this.request) { this.request = new FetchRequest(this, FetchMethod.get, this.location) this.request.perform() } @@ -216,8 +215,8 @@ export class Visit { } } - async getCachedSnapshot() { - const snapshot = (await this.view.getCachedSnapshotForLocation(this.location)) || this.getPreloadedSnapshot() + getCachedSnapshot() { + const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot() if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) { if (this.action == "restore" || snapshot.isPreviewable) { @@ -233,12 +232,13 @@ export class Visit { } hasCachedSnapshot() { - return this.cachedSnapshot != null + return this.getCachedSnapshot() != null } - async loadCachedSnapshot() { - if (this.cachedSnapshot) { - const isPreview = await this.shouldIssueRequest() + loadCachedSnapshot() { + const snapshot = this.getCachedSnapshot() + if (snapshot) { + const isPreview = this.shouldIssueRequest() this.render(async () => { this.cacheSnapshot() if (this.isSamePage) { @@ -246,7 +246,7 @@ export class Visit { } else { if (this.view.renderPromise) await this.view.renderPromise - await this.renderPageSnapshot(this.cachedSnapshot, isPreview) + await this.renderPageSnapshot(snapshot, isPreview) this.adapter.visitRendered(this) if (!isPreview) { @@ -391,10 +391,10 @@ export class Visit { return typeof this.response == "object" } - async shouldIssueRequest() { + shouldIssueRequest() { if (this.isSamePage) { return false - } else if (this.action === "restore") { + } else if (this.action == "restore") { return !this.hasCachedSnapshot() } else { return this.willRender diff --git a/src/core/native/browser_adapter.js b/src/core/native/browser_adapter.js index 2351d10e4..15da5dd41 100644 --- a/src/core/native/browser_adapter.js +++ b/src/core/native/browser_adapter.js @@ -27,7 +27,11 @@ export class BrowserAdapter { visitRequestStarted(visit) { this.progressBar.setValue(0) - this.showVisitProgressBarAfterDelay() + if (visit.hasCachedSnapshot() || visit.action != "restore") { + this.showVisitProgressBarAfterDelay() + } else { + this.showProgressBar() + } } visitRequestCompleted(visit) { diff --git a/src/tests/fixtures/disk_cache.html b/src/tests/fixtures/disk_cache.html deleted file mode 100644 index f8ba78a64..000000000 --- a/src/tests/fixtures/disk_cache.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - Turbo - - - - - -

Cached pages:

- - -

Links:

- - - - - diff --git a/src/tests/functional/disk_cache_tests.js b/src/tests/functional/disk_cache_tests.js deleted file mode 100644 index ca9bd6ec4..000000000 --- a/src/tests/functional/disk_cache_tests.js +++ /dev/null @@ -1,58 +0,0 @@ -import { test, expect } from "@playwright/test" -import { nextBody } from "../helpers/page" - -const path = "/src/tests/fixtures/disk_cache.html" - -test.beforeEach(async ({ page }) => { - await page.goto(path) -}) - -test("stores pages in the disk cache", async ({ page }) => { - await assertCachedURLs(page, []) - - page.click("#second-link") - await nextBody(page) - - await assertCachedURLs(page, ["http://localhost:9000/src/tests/fixtures/disk_cache.html"]) - - page.click("#third-link") - await nextBody(page) - - await assertCachedURLs(page, [ - "http://localhost:9000/src/tests/fixtures/disk_cache.html", - "http://localhost:9000/src/tests/fixtures/disk_cache.html?page=2" - ]) - - // Cache persists across reloads - await page.reload() - - await assertCachedURLs(page, [ - "http://localhost:9000/src/tests/fixtures/disk_cache.html", - "http://localhost:9000/src/tests/fixtures/disk_cache.html?page=2" - ]) -}) - -test("can clear the disk cache", async ({ page }) => { - page.click("#second-link") - await nextBody(page) - - await assertCachedURLs(page, ["http://localhost:9000/src/tests/fixtures/disk_cache.html"]) - - page.click("#clear-cache") - await assertCachedURLs(page, []) - - await page.reload() - await assertCachedURLs(page, []) -}) - -const assertCachedURLs = async (page, urls) => { - if (urls.length == 0) { - await expect(page.locator("#caches")).toBeEmpty() - } else { - await Promise.all( - urls.map((url) => { - return expect(page.locator("#caches")).toContainText(url) - }) - ) - } -} diff --git a/src/tests/functional/navigation_tests.js b/src/tests/functional/navigation_tests.js index 43b9a2359..e5c3986f8 100644 --- a/src/tests/functional/navigation_tests.js +++ b/src/tests/functional/navigation_tests.js @@ -195,13 +195,7 @@ test("following a POST form clears cache", async ({ page }) => { await page.click("#form-post-submit") await nextBeat() // 301 redirect response await nextBeat() // 200 response - - assert.equal(await page.textContent("h1"), "One") - await page.goBack() - await nextBeat() - - assert.equal(await page.textContent("h1"), "Navigation") assert.notOk(await hasSelector(page, "some-cached-element")) }) diff --git a/src/tests/functional/preloader_tests.js b/src/tests/functional/preloader_tests.js index 1337fa5fd..83293342a 100644 --- a/src/tests/functional/preloader_tests.js +++ b/src/tests/functional/preloader_tests.js @@ -8,11 +8,11 @@ test("preloads snapshot on initial load", async ({ page }) => { await nextBeat() assert.ok( - await page.evaluate(async () => { - const preloadedUrl = new URL("http://localhost:9000/src/tests/fixtures/preloaded.html") - const cache = window.Turbo.session.preloader.snapshotCache + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots - return await cache.has(preloadedUrl) + return preloadedUrl in cache }) ) }) @@ -27,11 +27,11 @@ test("preloads snapshot on page visit", async ({ page }) => { await nextBeat() assert.ok( - await page.evaluate(async () => { - const preloadedUrl = new URL("http://localhost:9000/src/tests/fixtures/preloaded.html") - const cache = window.Turbo.session.preloader.snapshotCache + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots - return await cache.has(preloadedUrl) + return preloadedUrl in cache }) ) }) @@ -43,11 +43,11 @@ test("navigates to preloaded snapshot from frame", async ({ page }) => { await nextBeat() assert.ok( - await page.evaluate(async () => { - const preloadedUrl = new URL("http://localhost:9000/src/tests/fixtures/preloaded.html") - const cache = window.Turbo.session.preloader.snapshotCache + await page.evaluate(() => { + const preloadedUrl = "http://localhost:9000/src/tests/fixtures/preloaded.html" + const cache = window.Turbo.session.preloader.snapshotCache.snapshots - return await cache.has(preloadedUrl) + return preloadedUrl in cache }) ) }) diff --git a/src/tests/unit/deprecated_adapter_support_tests.js b/src/tests/unit/deprecated_adapter_support_tests.js index 8d93650ef..799bc6100 100644 --- a/src/tests/unit/deprecated_adapter_support_tests.js +++ b/src/tests/unit/deprecated_adapter_support_tests.js @@ -50,7 +50,7 @@ test("visit proposal location includes deprecated absoluteURL property", async ( }) test("visit start location includes deprecated absoluteURL property", async () => { - await Turbo.navigator.startVisit(window.location.toString(), "123") + Turbo.navigator.startVisit(window.location.toString(), "123") assert.equal(adapter.locations.length, 1) const [location] = adapter.locations diff --git a/src/tests/unit/native_adapter_support_tests.js b/src/tests/unit/native_adapter_support_tests.js index 3926b0823..6c8c362d0 100644 --- a/src/tests/unit/native_adapter_support_tests.js +++ b/src/tests/unit/native_adapter_support_tests.js @@ -89,14 +89,14 @@ test("visit proposal external location is proposed to adapter", async () => { test("visit started notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) assert.equal(adapter.startedVisits.length, 1) const [visit] = adapter.startedVisits assert.equal(visit.location, locatable) }) -test("visit has cached snapshot returns boolean", async () => { +test("test visit has cached snapshot returns boolean", async () => { const locatable = window.location.toString() await Turbo.navigator.startVisit(locatable) @@ -108,7 +108,7 @@ test("visit has cached snapshot returns boolean", async () => { test("visit completed notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.complete() @@ -120,7 +120,7 @@ test("visit completed notifies adapter", async () => { test("visit request started notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.startRequest() @@ -133,7 +133,7 @@ test("visit request started notifies adapter", async () => { test("visit request completed notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.recordResponse({ statusCode: 200, responseHTML: "responseHtml", redirected: false }) @@ -146,7 +146,7 @@ test("visit request completed notifies adapter", async () => { test("visit request failed notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.recordResponse({ statusCode: 404, responseHTML: "responseHtml", redirected: false }) @@ -159,7 +159,7 @@ test("visit request failed notifies adapter", async () => { test("visit request finished notifies adapter", async () => { const locatable = window.location.toString() - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.finishRequest() @@ -190,7 +190,7 @@ test("visit follows redirect and proposes replace visit to adapter", async () => const locatable = window.location.toString() const redirectedLocation = "https://example.com" - await Turbo.navigator.startVisit(locatable) + Turbo.navigator.startVisit(locatable) const [startedVisit] = adapter.startedVisits startedVisit.redirectedToLocation = redirectedLocation