Skip to content
This repository was archived by the owner on Dec 14, 2023. It is now read-only.

Commit 36091d4

Browse files
committed
Use LRU for the cache
1 parent 8e482ec commit 36091d4

File tree

8 files changed

+268
-12
lines changed

8 files changed

+268
-12
lines changed

gulpfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ var gulp = require('gulp'),
1010

1111
var SOURCES = [
1212
'src/polyfill.js',
13+
'src/lru.js',
1314
'src/api.js',
1415
'src/cache.js',
1516
'src/utils.js',

src/cache.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,32 @@
55
/**
66
* Api cache
77
*/
8-
function ApiCache() {
9-
this.cache = {};
10-
this.states = {};
8+
function ApiCache(limit) {
9+
this.lru = new LRUCache(limit);
1110
}
1211

1312
ApiCache.prototype = {
1413

1514
get: function(key, cb) {
16-
var maybeEntry = this.cache[key];
15+
var maybeEntry = this.lru.get(key);
1716
if(maybeEntry && !this.isExpired(key)) {
1817
return cb(null, maybeEntry.data);
1918
}
2019
return cb();
2120
},
2221

2322
set: function(key, value, ttl, cb) {
24-
this.cache[key] = {
23+
this.lru.remove(key);
24+
this.lru.put(key, {
2525
data: value,
2626
expiredIn: ttl ? (Date.now() + (ttl * 1000)) : 0
27-
};
27+
});
2828

2929
return cb();
3030
},
3131

3232
isExpired: function(key) {
33-
var entry = this.cache[key];
33+
var entry = this.lru.find(key);
3434
if(entry) {
3535
return entry.expiredIn !== 0 && entry.expiredIn < Date.now();
3636
} else {
@@ -39,12 +39,12 @@
3939
},
4040

4141
remove: function(key, cb) {
42-
delete this.cache[key];
42+
this.lru.remove(key);
4343
return cb();
4444
},
4545

46-
clear: function(key, cb) {
47-
this.cache = {};
46+
clear: function(cb) {
47+
this.lru.removeAll();
4848
return cb();
4949
}
5050
};

src/lru.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
/**
2+
* A doubly linked list-based Least Recently Used (LRU) cache. Will keep most
3+
* recently used items while discarding least recently used items when its limit
4+
* is reached.
5+
*
6+
* Licensed under MIT. Copyright (c) 2010 Rasmus Andersson <http://hunch.se/>
7+
* See README.md for details.
8+
*
9+
* Illustration of the design:
10+
*
11+
* entry entry entry entry
12+
* ______ ______ ______ ______
13+
* | head |.newer => | |.newer => | |.newer => | tail |
14+
* | A | | B | | C | | D |
15+
* |______| <= older.|______| <= older.|______| <= older.|______|
16+
*
17+
* removed <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- <-- added
18+
*/
19+
function LRUCache (limit) {
20+
// Current size of the cache. (Read-only).
21+
this.size = 0;
22+
// Maximum number of items this cache can hold.
23+
this.limit = limit;
24+
this._keymap = {};
25+
}
26+
27+
/**
28+
* Put <value> into the cache associated with <key>. Returns the entry which was
29+
* removed to make room for the new entry. Otherwise undefined is returned
30+
* (i.e. if there was enough room already).
31+
*/
32+
LRUCache.prototype.put = function(key, value) {
33+
var entry = {key:key, value:value};
34+
// Note: No protection agains replacing, and thus orphan entries. By design.
35+
this._keymap[key] = entry;
36+
if (this.tail) {
37+
// link previous tail to the new tail (entry)
38+
this.tail.newer = entry;
39+
entry.older = this.tail;
40+
} else {
41+
// we're first in -- yay
42+
this.head = entry;
43+
}
44+
// add new entry to the end of the linked list -- it's now the freshest entry.
45+
this.tail = entry;
46+
if (this.size === this.limit) {
47+
// we hit the limit -- remove the head
48+
return this.shift();
49+
} else {
50+
// increase the size counter
51+
this.size++;
52+
}
53+
};
54+
55+
/**
56+
* Purge the least recently used (oldest) entry from the cache. Returns the
57+
* removed entry or undefined if the cache was empty.
58+
*
59+
* If you need to perform any form of finalization of purged items, this is a
60+
* good place to do it. Simply override/replace this function:
61+
*
62+
* var c = new LRUCache(123);
63+
* c.shift = function() {
64+
* var entry = LRUCache.prototype.shift.call(this);
65+
* doSomethingWith(entry);
66+
* return entry;
67+
* }
68+
*/
69+
LRUCache.prototype.shift = function() {
70+
// todo: handle special case when limit == 1
71+
var entry = this.head;
72+
if (entry) {
73+
if (this.head.newer) {
74+
this.head = this.head.newer;
75+
this.head.older = undefined;
76+
} else {
77+
this.head = undefined;
78+
}
79+
// Remove last strong reference to <entry> and remove links from the purged
80+
// entry being returned:
81+
entry.newer = entry.older = undefined;
82+
// delete is slow, but we need to do this to avoid uncontrollable growth:
83+
delete this._keymap[entry.key];
84+
}
85+
return entry;
86+
};
87+
88+
/**
89+
* Get and register recent use of <key>. Returns the value associated with <key>
90+
* or undefined if not in cache.
91+
*/
92+
LRUCache.prototype.get = function(key, returnEntry) {
93+
// First, find our cache entry
94+
var entry = this._keymap[key];
95+
if (entry === undefined) return; // Not cached. Sorry.
96+
// As <key> was found in the cache, register it as being requested recently
97+
if (entry === this.tail) {
98+
// Already the most recenlty used entry, so no need to update the list
99+
return returnEntry ? entry : entry.value;
100+
}
101+
// HEAD--------------TAIL
102+
// <.older .newer>
103+
// <--- add direction --
104+
// A B C <D> E
105+
if (entry.newer) {
106+
if (entry === this.head)
107+
this.head = entry.newer;
108+
entry.newer.older = entry.older; // C <-- E.
109+
}
110+
if (entry.older)
111+
entry.older.newer = entry.newer; // C. --> E
112+
entry.newer = undefined; // D --x
113+
entry.older = this.tail; // D. --> E
114+
if (this.tail)
115+
this.tail.newer = entry; // E. <-- D
116+
this.tail = entry;
117+
return returnEntry ? entry : entry.value;
118+
};
119+
120+
// ----------------------------------------------------------------------------
121+
// Following code is optional and can be removed without breaking the core
122+
// functionality.
123+
124+
/**
125+
* Check if <key> is in the cache without registering recent use. Feasible if
126+
* you do not want to chage the state of the cache, but only "peek" at it.
127+
* Returns the entry associated with <key> if found, or undefined if not found.
128+
*/
129+
LRUCache.prototype.find = function(key) {
130+
return this._keymap[key];
131+
};
132+
133+
/**
134+
* Update the value of entry with <key>. Returns the old value, or undefined if
135+
* entry was not in the cache.
136+
*/
137+
LRUCache.prototype.set = function(key, value) {
138+
var oldvalue, entry = this.get(key, true);
139+
if (entry) {
140+
oldvalue = entry.value;
141+
entry.value = value;
142+
} else {
143+
oldvalue = this.put(key, value);
144+
if (oldvalue) oldvalue = oldvalue.value;
145+
}
146+
return oldvalue;
147+
};
148+
149+
/**
150+
* Remove entry <key> from cache and return its value. Returns undefined if not
151+
* found.
152+
*/
153+
LRUCache.prototype.remove = function(key) {
154+
var entry = this._keymap[key];
155+
if (!entry) return;
156+
delete this._keymap[entry.key]; // need to do delete unfortunately
157+
if (entry.newer && entry.older) {
158+
// relink the older entry with the newer entry
159+
entry.older.newer = entry.newer;
160+
entry.newer.older = entry.older;
161+
} else if (entry.newer) {
162+
// remove the link to us
163+
entry.newer.older = undefined;
164+
// link the newer entry to head
165+
this.head = entry.newer;
166+
} else if (entry.older) {
167+
// remove the link to us
168+
entry.older.newer = undefined;
169+
// link the newer entry to head
170+
this.tail = entry.older;
171+
} else {// if(entry.older === undefined && entry.newer === undefined) {
172+
this.head = this.tail = undefined;
173+
}
174+
175+
this.size--;
176+
return entry.value;
177+
};
178+
179+
/** Removes all entries */
180+
LRUCache.prototype.removeAll = function() {
181+
// This should be safe, as we never expose strong refrences to the outside
182+
this.head = this.tail = undefined;
183+
this.size = 0;
184+
this._keymap = {};
185+
};
186+
187+
/**
188+
* Return an array containing all keys of entries stored in the cache object, in
189+
* arbitrary order.
190+
*/
191+
if (typeof Object.keys === 'function') {
192+
LRUCache.prototype.keys = function() { return Object.keys(this._keymap); };
193+
} else {
194+
LRUCache.prototype.keys = function() {
195+
var keys = [];
196+
for (var k in this._keymap) keys.push(k);
197+
return keys;
198+
};
199+
}
200+
201+
/**
202+
* Call `fun` for each entry. Starting with the newest entry if `desc` is a true
203+
* value, otherwise starts with the oldest (head) enrty and moves towards the
204+
* tail.
205+
*
206+
* `fun` is called with 3 arguments in the context `context`:
207+
* `fun.call(context, Object key, Object value, LRUCache self)`
208+
*/
209+
LRUCache.prototype.forEach = function(fun, context, desc) {
210+
var entry;
211+
if (context === true) { desc = true; context = undefined; }
212+
else if (typeof context !== 'object') context = this;
213+
if (desc) {
214+
entry = this.tail;
215+
while (entry) {
216+
fun.call(context, entry.key, entry.value, this);
217+
entry = entry.older;
218+
}
219+
} else {
220+
entry = this.head;
221+
while (entry) {
222+
fun.call(context, entry.key, entry.value, this);
223+
entry = entry.newer;
224+
}
225+
}
226+
};
227+
228+
/** Returns a JSON (array) representation */
229+
LRUCache.prototype.toJSON = function() {
230+
var s = [], entry = this.head;
231+
while (entry) {
232+
s.push({key:entry.key.toJSON(), value:entry.value.toJSON()});
233+
entry = entry.newer;
234+
}
235+
return s;
236+
};
237+
238+
/** Returns a String representation */
239+
LRUCache.prototype.toString = function() {
240+
var s = '', entry = this.head;
241+
while (entry) {
242+
s += String(entry.key)+':'+entry.value;
243+
entry = entry.newer;
244+
if (entry)
245+
s += ' < ';
246+
}
247+
return s;
248+
};
249+
250+
// Export ourselves
251+
if (typeof this === 'object') this.LRUCache = LRUCache;

test/doc.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<script>mocha.setup('bdd')</script>
1414
<!-- Load local lib and tests. -->
1515
<script src="../src/api.js"></script>
16+
<script src="../src/lru.js"></script>
1617
<script src="../src/cache.js"></script>
1718
<script src="../src/utils.js"></script>
1819
<script src="../src/documents.js"></script>

test/fragments.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<script>mocha.setup('bdd')</script>
1414
<!-- Load local lib and tests. -->
1515
<script src="../src/api.js"></script>
16+
<script src="../src/lru.js"></script>
1617
<script src="../src/cache.js"></script>
1718
<script src="../src/utils.js"></script>
1819
<script src="../src/documents.js"></script>

test/test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<script>mocha.setup('bdd')</script>
1414
<!-- Load local lib and tests. -->
1515
<script src="../src/api.js"></script>
16+
<script src="../src/lru.js"></script>
1617
<script src="../src/cache.js"></script>
1718
<script src="../src/utils.js"></script>
1819
<script src="../src/documents.js"></script>

test/test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,15 +373,15 @@
373373
Prismic.Api(testRepository, function (err, Api) {
374374
if (err) return done(err);
375375
var form = Api.form('products').ref(Api.master()).query('[[:d = at(my.product.flavour, "Caramel")]]');
376-
var olderKeys = Object.keys(Api.apiCache.cache);
376+
var olderKeys = Api.apiCache.lru.keys();
377377
form.submit(function (err, response) {
378378
if (err) {
379379
console.log(err);
380380
return done(err);
381381
}
382382

383383
var key = null;
384-
Object.keys(Api.apiCache.cache).forEach(function (candidate) {
384+
Api.apiCache.lru.keys().forEach(function (candidate) {
385385
if (olderKeys.indexOf(candidate) == -1) {
386386
key = candidate;
387387
}

test/unit.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<script>mocha.setup('bdd')</script>
1414
<!-- Load local lib and tests. -->
1515
<script src="../src/api.js"></script>
16+
<script src="../src/lru.js"></script>
1617
<script src="../src/cache.js"></script>
1718
<script src="../src/utils.js"></script>
1819
<script src="../src/documents.js"></script>

0 commit comments

Comments
 (0)