Skip to content

Commit 0419ed8

Browse files
committed
PrefetchCache: extract and re-use LRUCache from SnapshotCache
What --- First, extract the `LRUCache` (named after the ["least recently used" caching policy][LRU]) class from the `SnapshotCache`. The implementation is largely unchanged, with the following exceptions: 1. accept `toCacheKey` argument to coerce key values 2. rename `this.snapshot` property to a more generic `this.entries` 3. rename `snapshot` arguments to `entry` 4. rename `location` arguments to `key` Next, implement `SnapshotCache` in terms of the `LRUCache`. Since it served as the original implementation, it is mostly an empty class that passes the `toCacheKey` utility function to the constructor, and provides a `snapshots` property alias that returns the `this.entries` property. Also implement the `PrefetchCache` as an extension of the `LRUCache`. Rather than storing a reference to the "hot" entry, construct a cache of size 1. When a new entry is put into the cache, the old entry is expired. To supplement the "least recently used" purge strategy, also declare a [setInterval][] function to purge expired entries every `50ms`. Finally, change all `PrefetchCache.get` call sites to pass the [URL][] instance directly, rather than the `String`. Once passed as the argument, the `PrefetchCache.get` implementation uses the existing `toCacheKey` utility function already used by the `SnapshotCache`. Rename any `PrefetchCache.setLater` call sites to instead invoke `PrefetchCache.putLater` (named to match the `SnapshotCache` and `LRUCache` "put" method). Why --- By adjusting the underlying `PrefetchCache` implementation to expand upon an existing concept, it opens up possibilities for further customization and configuration. Future changes could support client-provided cache size and purge interval configuration to adjust link prefetching behavior intervals to suit application-specific. [LRU]: https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU [setInterval]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval [URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
1 parent 9a79b30 commit 0419ed8

File tree

4 files changed

+101
-65
lines changed

4 files changed

+101
-65
lines changed

src/core/drive/prefetch_cache.js

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,48 @@
1+
import { LRUCache } from "../lru_cache"
2+
import { toCacheKey } from "../url"
3+
14
const PREFETCH_DELAY = 100
25

3-
class PrefetchCache {
6+
class PrefetchCache extends LRUCache {
47
#prefetchTimeout = null
5-
#prefetched = null
8+
#maxAges = {}
69

7-
get(url) {
8-
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
9-
return this.#prefetched.request
10-
}
10+
constructor(size = 1, prefetchDelay = PREFETCH_DELAY) {
11+
super(size, toCacheKey)
12+
this.prefetchDelay = prefetchDelay
1113
}
1214

13-
setLater(url, request, ttl) {
14-
this.clear()
15-
15+
putLater(url, request, ttl) {
1616
this.#prefetchTimeout = setTimeout(() => {
1717
request.perform()
18-
this.set(url, request, ttl)
18+
this.put(url, request, ttl)
1919
this.#prefetchTimeout = null
20-
}, PREFETCH_DELAY)
20+
}, this.prefetchDelay)
2121
}
2222

23-
set(url, request, ttl) {
24-
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) }
23+
put(url, request, ttl = cacheTtl) {
24+
super.put(url, request)
25+
this.#maxAges[toCacheKey(url)] = new Date(new Date().getTime() + ttl)
2526
}
2627

2728
clear() {
29+
super.clear()
2830
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout)
29-
this.#prefetched = null
31+
}
32+
33+
evict(key) {
34+
super.evict(key)
35+
delete this.#maxAges[key]
36+
}
37+
38+
has(key) {
39+
if (super.has(key)) {
40+
const maxAge = this.#maxAges[toCacheKey(key)]
41+
42+
return maxAge && maxAge > Date.now()
43+
} else {
44+
return false
45+
}
3046
}
3147
}
3248

src/core/drive/snapshot_cache.js

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,12 @@
11
import { toCacheKey } from "../url"
2+
import { LRUCache } from "../lru_cache"
23

3-
export class SnapshotCache {
4-
keys = []
5-
snapshots = {}
6-
4+
export class SnapshotCache extends LRUCache {
75
constructor(size) {
8-
this.size = size
9-
}
10-
11-
has(location) {
12-
return toCacheKey(location) in this.snapshots
13-
}
14-
15-
get(location) {
16-
if (this.has(location)) {
17-
const snapshot = this.read(location)
18-
this.touch(location)
19-
return snapshot
20-
}
21-
}
22-
23-
put(location, snapshot) {
24-
this.write(location, snapshot)
25-
this.touch(location)
26-
return snapshot
27-
}
28-
29-
clear() {
30-
this.snapshots = {}
31-
}
32-
33-
// Private
34-
35-
read(location) {
36-
return this.snapshots[toCacheKey(location)]
37-
}
38-
39-
write(location, snapshot) {
40-
this.snapshots[toCacheKey(location)] = snapshot
41-
}
42-
43-
touch(location) {
44-
const key = toCacheKey(location)
45-
const index = this.keys.indexOf(key)
46-
if (index > -1) this.keys.splice(index, 1)
47-
this.keys.unshift(key)
48-
this.trim()
6+
super(size, toCacheKey)
497
}
508

51-
trim() {
52-
for (const key of this.keys.splice(this.size)) {
53-
delete this.snapshots[key]
54-
}
9+
get snapshots() {
10+
return this.entries
5511
}
5612
}

src/core/lru_cache.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const identity = key => key
2+
3+
export class LRUCache {
4+
keys = []
5+
entries = {}
6+
#toCacheKey
7+
8+
constructor(size, toCacheKey = identity) {
9+
this.size = size
10+
this.#toCacheKey = toCacheKey
11+
}
12+
13+
has(key) {
14+
return this.#toCacheKey(key) in this.entries
15+
}
16+
17+
get(key) {
18+
if (this.has(key)) {
19+
const entry = this.read(key)
20+
this.touch(key)
21+
return entry
22+
}
23+
}
24+
25+
put(key, entry) {
26+
this.write(key, entry)
27+
this.touch(key)
28+
return entry
29+
}
30+
31+
clear() {
32+
for (const key of Object.keys(this.entries)) {
33+
this.evict(key)
34+
}
35+
}
36+
37+
// Private
38+
39+
read(key) {
40+
return this.entries[this.#toCacheKey(key)]
41+
}
42+
43+
write(key, entry) {
44+
this.entries[this.#toCacheKey(key)] = entry
45+
}
46+
47+
touch(key) {
48+
key = this.#toCacheKey(key)
49+
const index = this.keys.indexOf(key)
50+
if (index > -1) this.keys.splice(index, 1)
51+
this.keys.unshift(key)
52+
this.trim()
53+
}
54+
55+
trim() {
56+
for (const key of this.keys.splice(this.size)) {
57+
this.evict(key)
58+
}
59+
}
60+
61+
evict(key) {
62+
delete this.entries[key]
63+
}
64+
}

src/observers/link_prefetch_observer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export class LinkPrefetchObserver {
8080

8181
fetchRequest.fetchOptions.priority = "low"
8282

83-
prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl)
83+
prefetchCache.putLater(location, fetchRequest, this.#cacheTtl)
8484
}
8585
}
8686
}
@@ -96,7 +96,7 @@ export class LinkPrefetchObserver {
9696

9797
#tryToUsePrefetchedRequest = (event) => {
9898
if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "GET") {
99-
const cached = prefetchCache.get(event.detail.url.toString())
99+
const cached = prefetchCache.get(event.detail.url)
100100

101101
if (cached) {
102102
// User clicked link, use cache response

0 commit comments

Comments
 (0)