File size: 3,560 Bytes
b91e262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { deleteMapEntry } from './cache-map'
import type { UnknownMapEntry } from './cache-map'

// We use an LRU for memory management. We must update this whenever we add or
// remove a new cache entry, or when an entry changes size.

let head: UnknownMapEntry | null = null
let didScheduleCleanup: boolean = false
let lruSize: number = 0

// TODO: I chose the max size somewhat arbitrarily. Consider setting this based
// on navigator.deviceMemory, or some other heuristic. We should make this
// customizable via the Next.js config, too.
const maxLruSize = 50 * 1024 * 1024 // 50 MB

export function lruPut(node: UnknownMapEntry) {
  if (head === node) {
    // Already at the head
    return
  }
  const prev = node.prev
  const next = node.next
  if (next === null || prev === null) {
    // This is an insertion
    lruSize += node.size
    // Whenever we add an entry, we need to check if we've exceeded the
    // max size. We don't evict entries immediately; they're evicted later in
    // an asynchronous task.
    ensureCleanupIsScheduled()
  } else {
    // This is a move. Remove from its current position.
    prev.next = next
    next.prev = prev
  }

  // Move to the front of the list
  if (head === null) {
    // This is the first entry
    node.prev = node
    node.next = node
  } else {
    // Add to the front of the list
    const tail = head.prev
    node.prev = tail
    // In practice, this is never null, but that isn't encoded in the type
    if (tail !== null) {
      tail.next = node
    }
    node.next = head
    head.prev = node
  }
  head = node
}

export function updateLruSize(node: UnknownMapEntry, newNodeSize: number) {
  // This is a separate function from `put` so that we can resize the entry
  // regardless of whether it's currently being tracked by the LRU.
  const prevNodeSize = node.size
  node.size = newNodeSize
  if (node.next === null) {
    // This entry is not currently being tracked by the LRU.
    return
  }
  // Update the total LRU size
  lruSize = lruSize - prevNodeSize + newNodeSize
  ensureCleanupIsScheduled()
}

export function deleteFromLru(deleted: UnknownMapEntry) {
  const next = deleted.next
  const prev = deleted.prev
  if (next !== null && prev !== null) {
    lruSize -= deleted.size

    deleted.next = null
    deleted.prev = null

    // Remove from the list
    if (head === deleted) {
      // Update the head
      if (next === head) {
        // This was the last entry
        head = null
      } else {
        head = next
      }
    } else {
      prev.next = next
      next.prev = prev
    }
  } else {
    // Already deleted
  }
}

function ensureCleanupIsScheduled() {
  if (didScheduleCleanup || lruSize <= maxLruSize) {
    return
  }
  didScheduleCleanup = true
  requestCleanupCallback(cleanup)
}

function cleanup() {
  didScheduleCleanup = false

  // Evict entries until we're at 90% capacity. We can assume this won't
  // infinite loop because even if `maxLruSize` were 0, eventually
  // `deleteFromLru` sets `head` to `null` when we run out entries.
  const ninetyPercentMax = maxLruSize * 0.9
  while (lruSize > ninetyPercentMax && head !== null) {
    const tail = head.prev
    // In practice, this is never null, but that isn't encoded in the type
    if (tail !== null) {
      // Delete the entry from the map. In turn, this will remove it from
      // the LRU.
      deleteMapEntry(tail)
    }
  }
}

const requestCleanupCallback =
  typeof requestIdleCallback === 'function'
    ? requestIdleCallback
    : (cb: () => void) => setTimeout(cb, 0)