-
Notifications
You must be signed in to change notification settings - Fork 437
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit extends the Turbo cache API to allow for the use of different cache stores. The default store is still the in-memory store, but a new broser cache store is now available. The browser cache store uses the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) to store and retrieve snapshotsfrom the browser's cache. This allows for the snapshots to be persisted across different tabs, page reloads and even browser restarts. The browser cache store is not enabled by default. To enable it, you need to set the `Turbo.cache.store` property to `"http-cache"`. ```js Turbo.cache.store = "http-cache" ``` This is also a stepping stone to implement offline support with Service Workers. With a Service Worker in place, and the Browser cache store enabled, we can still serve cached snapshots even when the browser is offline.
- Loading branch information
Showing
12 changed files
with
291 additions
and
63 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { PageSnapshot } from "./page_snapshot" | ||
|
||
export abstract class CacheStore { | ||
abstract has(location: URL): Promise<boolean> | ||
abstract get(location: URL): Promise<PageSnapshot | undefined> | ||
abstract put(location: URL, snapshot: PageSnapshot): Promise<PageSnapshot> | ||
abstract clear(): Promise<void> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { CacheStore } from "../cache_store" | ||
import { PageSnapshot } from "../page_snapshot" | ||
|
||
export class BrowserCacheStore extends CacheStore { | ||
_version = "v1" | ||
storage!: Cache | ||
|
||
constructor() { | ||
super() | ||
this.initialize() | ||
} | ||
|
||
async initialize() { | ||
this.storage = await caches.open(`turbo-${this.version}`) | ||
} | ||
|
||
async has(location: URL) { | ||
return (await this.storage.match(location)) !== undefined | ||
} | ||
|
||
async get(location: URL) { | ||
const response = await this.storage.match(location) | ||
if (response && response.ok) { | ||
const html = await response.text() | ||
return PageSnapshot.fromHTMLString(html) | ||
} | ||
} | ||
|
||
async put(location: URL, snapshot: PageSnapshot) { | ||
const response = new Response(snapshot.html, { | ||
status: 200, | ||
statusText: "OK", | ||
headers: { | ||
"Content-Type": "text/html", | ||
}, | ||
}) | ||
await this.storage.put(location, response) | ||
return snapshot | ||
} | ||
|
||
async clear() { | ||
const keys = await this.storage.keys() | ||
await Promise.all(keys.map((key) => this.storage.delete(key))) | ||
} | ||
|
||
set version(value: string) { | ||
this._version = value | ||
this.initialize() | ||
} | ||
|
||
get version() { | ||
return this._version | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { toCacheKey } from "../../url" | ||
import { PageSnapshot } from "../page_snapshot" | ||
import { CacheStore } from "../cache_store" | ||
|
||
export class MemoryStore extends CacheStore { | ||
readonly keys: string[] = [] | ||
readonly size: number | ||
snapshots: { [url: string]: PageSnapshot } = {} | ||
|
||
constructor(size: number) { | ||
super() | ||
this.size = size | ||
} | ||
|
||
async has(location: URL) { | ||
return toCacheKey(location) in this.snapshots | ||
} | ||
|
||
async get(location: URL): Promise<PageSnapshot | undefined> { | ||
if (await this.has(location)) { | ||
const snapshot = this.read(location) | ||
this.touch(location) | ||
return snapshot | ||
} | ||
} | ||
|
||
async put(location: URL, snapshot: PageSnapshot): Promise<PageSnapshot> { | ||
this.write(location, snapshot) | ||
this.touch(location) | ||
return snapshot | ||
} | ||
|
||
async clear() { | ||
this.snapshots = {} | ||
} | ||
|
||
// Private | ||
|
||
read(location: URL) { | ||
return this.snapshots[toCacheKey(location)] | ||
} | ||
|
||
write(location: URL, snapshot: PageSnapshot) { | ||
this.snapshots[toCacheKey(location)] = snapshot | ||
} | ||
|
||
touch(location: URL) { | ||
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] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,58 +1,37 @@ | ||
import { toCacheKey } from "../url" | ||
import { PageSnapshot } from "./page_snapshot" | ||
import { CacheStore } from "./cache_store" | ||
import { BrowserCacheStore } from "./cache_stores/browser_cache_store" | ||
import { MemoryStore } from "./cache_stores/memory_store" | ||
|
||
export class SnapshotCache { | ||
readonly keys: string[] = [] | ||
readonly size: number | ||
snapshots: { [url: string]: PageSnapshot } = {} | ||
|
||
constructor(size: number) { | ||
this.size = size | ||
static currentStore: CacheStore = new MemoryStore(10) | ||
|
||
static setStore(storeName: string) { | ||
switch (storeName) { | ||
case "memory": | ||
SnapshotCache.currentStore = new MemoryStore(10) | ||
break | ||
case "browser-cache": | ||
SnapshotCache.currentStore = new BrowserCacheStore() | ||
break | ||
default: | ||
throw new Error(`Invalid store name: ${storeName}`) | ||
} | ||
} | ||
|
||
has(location: URL) { | ||
return toCacheKey(location) in this.snapshots | ||
return SnapshotCache.currentStore.has(location) | ||
} | ||
|
||
get(location: URL): PageSnapshot | undefined { | ||
if (this.has(location)) { | ||
const snapshot = this.read(location) | ||
this.touch(location) | ||
return snapshot | ||
} | ||
get(location: URL) { | ||
return SnapshotCache.currentStore.get(location) | ||
} | ||
|
||
put(location: URL, snapshot: PageSnapshot) { | ||
this.write(location, snapshot) | ||
this.touch(location) | ||
return snapshot | ||
return SnapshotCache.currentStore.put(location, snapshot) | ||
} | ||
|
||
clear() { | ||
this.snapshots = {} | ||
} | ||
|
||
// Private | ||
|
||
read(location: URL) { | ||
return this.snapshots[toCacheKey(location)] | ||
} | ||
|
||
write(location: URL, snapshot: PageSnapshot) { | ||
this.snapshots[toCacheKey(location)] = snapshot | ||
} | ||
|
||
touch(location: URL) { | ||
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<!DOCTYPE html> | ||
<html id="html" data-skip-event-details="turbo:submit-start turbo:submit-end"> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="csp-nonce" content="123"> | ||
<link rel="icon" href="data:;base64,iVBORw0KGgo="> | ||
<title>Turbo</title> | ||
<script src="/dist/turbo.es2017-umd.js" data-turbo-track="reload"></script> | ||
<script src="/src/tests/fixtures/test.js"></script> | ||
<script> | ||
Turbo.cache.store = "browser-cache" | ||
|
||
document.addEventListener("turbo:load", async () => { | ||
await new Promise(resolve => setTimeout(resolve, 100)) | ||
|
||
const cachesList = document.getElementById("caches") | ||
|
||
const cache = await caches.open("turbo-v1") | ||
const keys = await cache.keys() | ||
cachesList.innerHTML = keys.map(key => `<li>${key.url}</li>`).join("") | ||
|
||
const clearCacheButton = document.getElementById("clear-cache") | ||
clearCacheButton.addEventListener("click", async (event) => { | ||
await Turbo.cache.clear() | ||
cachesList.innerHTML = "" | ||
}) | ||
}) | ||
</script> | ||
</head> | ||
<body> | ||
<h1>Cached pages:</h1> | ||
<ul id="caches"></ul> | ||
|
||
<h3>Links:</h3> | ||
<ul> | ||
<li> | ||
<a id="first-link" href="./browser_cache.html">First HTTP cached page</a> | ||
</li> | ||
<li> | ||
<a id="second-link" href="./browser_cache.html?page=2">Second HTTP cached page</a> | ||
</li> | ||
<li> | ||
<a id="third-link" href="./browser_cache.html?page=3">Third HTTP cached page</a> | ||
</li> | ||
</ul> | ||
|
||
<button id="clear-cache">Clear cache</button> | ||
</body> | ||
</html> |
Oops, something went wrong.