diff --git a/src/core/cache.js b/src/core/cache.js index 2c163e6f4..c88bdcd29 100644 --- a/src/core/cache.js +++ b/src/core/cache.js @@ -1,4 +1,5 @@ import { setMetaContent } from "../util" +import { SnapshotCache } from "./drive/snapshot_cache" export class Cache { constructor(session) { @@ -6,7 +7,7 @@ export class Cache { } clear() { - this.session.clearCache() + this.store.clear() } resetCacheControl() { @@ -21,6 +22,18 @@ 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 new file mode 100644 index 000000000..285caa5d9 --- /dev/null +++ b/src/core/drive/cache_stores/disk_store.js @@ -0,0 +1,64 @@ +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 new file mode 100644 index 000000000..3ec8ae0b1 --- /dev/null +++ b/src/core/drive/cache_stores/memory_store.js @@ -0,0 +1,56 @@ +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/page_snapshot.js b/src/core/drive/page_snapshot.js index 575cb25eb..47488d785 100644 --- a/src/core/drive/page_snapshot.js +++ b/src/core/drive/page_snapshot.js @@ -40,6 +40,10 @@ export class PageSnapshot extends Snapshot { return new PageSnapshot(clonedElement, this.headSnapshot) } + 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 1b95bb1d1..09a7299c6 100644 --- a/src/core/drive/page_view.js +++ b/src/core/drive/page_view.js @@ -6,7 +6,7 @@ import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" export class PageView extends View { - snapshotCache = new SnapshotCache(10) + snapshotCache = new SnapshotCache() lastRenderedLocation = new URL(location.href) forceReloaded = false @@ -32,6 +32,10 @@ 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 23871a530..b971cea19 100644 --- a/src/core/drive/preloader.js +++ b/src/core/drive/preloader.js @@ -30,9 +30,7 @@ export class Preloader { async preloadURL(link) { const location = new URL(link.href) - if (this.snapshotCache.has(location)) { - return - } + if (await 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 6ed37e8fd..8e8c53c02 100644 --- a/src/core/drive/snapshot_cache.js +++ b/src/core/drive/snapshot_cache.js @@ -1,56 +1,35 @@ -import { toCacheKey } from "../url" +import { DiskStore } from "./cache_stores/disk_store" +import { MemoryStore } from "./cache_stores/memory_store" export class SnapshotCache { - keys = [] - snapshots = {} - - constructor(size) { - this.size = size + 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}`) + } } has(location) { - return toCacheKey(location) in this.snapshots + return SnapshotCache.currentStore.has(location) } get(location) { - if (this.has(location)) { - const snapshot = this.read(location) - this.touch(location) - return snapshot - } + return SnapshotCache.currentStore.get(location) } put(location, snapshot) { - this.write(location, snapshot) - this.touch(location) - return snapshot + return SnapshotCache.currentStore.put(location, snapshot) } 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] - } + return SnapshotCache.currentStore.clear() } } diff --git a/src/core/drive/visit.js b/src/core/drive/visit.js index 7fc494c19..d1929b03e 100644 --- a/src/core/drive/visit.js +++ b/src/core/drive/visit.js @@ -215,8 +215,8 @@ export class Visit { } } - getCachedSnapshot() { - const snapshot = this.view.getCachedSnapshotForLocation(this.location) || this.getPreloadedSnapshot() + async getCachedSnapshot() { + const snapshot = (await this.view.getCachedSnapshotForLocation(this.location)) || this.getPreloadedSnapshot() if (snapshot && (!getAnchor(this.location) || snapshot.hasAnchor(getAnchor(this.location)))) { if (this.action == "restore" || snapshot.isPreviewable) { @@ -235,8 +235,8 @@ export class Visit { return this.getCachedSnapshot() != null } - loadCachedSnapshot() { - const snapshot = this.getCachedSnapshot() + async loadCachedSnapshot() { + const snapshot = await this.getCachedSnapshot() if (snapshot) { const isPreview = this.shouldIssueRequest() this.render(async () => { diff --git a/src/tests/fixtures/disk_cache.html b/src/tests/fixtures/disk_cache.html new file mode 100644 index 000000000..f8ba78a64 --- /dev/null +++ b/src/tests/fixtures/disk_cache.html @@ -0,0 +1,49 @@ + + +
+ + + +