Skip to content

Commit de490a9

Browse files
committed
Initial commit
0 parents  commit de490a9

File tree

13 files changed

+601
-0
lines changed

13 files changed

+601
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package-lock.json
2+
node_modules
3+
.idea
4+
yarn.lock

LICENSE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2018 Jesper Svennevid
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is
8+
furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19+
SOFTWARE.

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# layer-cache
2+
3+
Simple layered caching for Node.
4+
5+
## Getting Started
6+
7+
## Versioning
8+
9+
We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/microcode/layer-cache/tags).
10+
11+
## License
12+
13+
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
14+

index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
exports.BaseCache = require('lib/base.js');
2+
exports.SerializingCache = require('./lib/serializing');
3+
exports.MemoryCache = require('lib/memory.js');
4+
exports.OnionCache = require('lib/onion.js');

lib/base.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class BaseCache {
2+
get(key) {
3+
return new Promise((resolve, reject) => {
4+
reject(new Error("Method 'get' not implememented"))
5+
});
6+
}
7+
8+
set(key, value) {
9+
return new Promise((resolve, reject) => {
10+
reject(new Error("Method 'set' not implememented"))
11+
});
12+
}
13+
14+
del(key) {
15+
return new Promise((resolve, reject) => {
16+
reject(new Error("Method 'del' not implememented"))
17+
});
18+
}
19+
}
20+
21+
exports.BaseCache = BaseCache;

lib/memory.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { BaseCache } = require('./base');
2+
const LRUCache = require('lru-cache');
3+
4+
class MemoryCache extends BaseCache {
5+
constructor(options) {
6+
super();
7+
8+
options = options || {};
9+
this.lru = LRUCache({
10+
max: options.max || 64,
11+
maxAge: options.maxAge || 1000 * 60 * 5
12+
});
13+
}
14+
15+
get(key) {
16+
return new Promise((resolve) => resolve(this.lru.get(key)));
17+
}
18+
19+
set(key, value) {
20+
return new Promise((resolve) => {
21+
this.lru.set(key, value);
22+
resolve();
23+
});
24+
}
25+
26+
del(key) {
27+
return new Promise((resolve) => {
28+
this.lru.del(key);
29+
resolve();
30+
});
31+
}
32+
}
33+
34+
exports.MemoryCache = MemoryCache;

lib/onion.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const { BaseCache } = require('./base');
2+
3+
class OnionCache extends BaseCache {
4+
constructor(...caches) {
5+
super();
6+
7+
this.caches = caches;
8+
}
9+
10+
get(key) {
11+
return this.__get(key, 0);
12+
}
13+
14+
__get(key, index) {
15+
if (index >= this.caches.length) {
16+
return Promise.resolve();
17+
}
18+
19+
const cache = this.caches[index];
20+
return cache.get(key).then((result) => {
21+
if (!result) {
22+
return this.__get(key, index + 1);
23+
}
24+
return result;
25+
});
26+
}
27+
28+
set(key, value) {
29+
return this.__set(key, value, 0);
30+
}
31+
32+
__set(key, value, index) {
33+
if (index >= this.caches.length) {
34+
return Promise.resolve();
35+
}
36+
37+
const cache = this.caches[index];
38+
return cache.set(key, value).then(() => {
39+
return this.__set(key, value, index + 1);
40+
});
41+
}
42+
43+
del(key) {
44+
return this.__del(key, 0);
45+
}
46+
47+
__del(key, index) {
48+
if (index >= this.caches.length) {
49+
return Promise.resolve();
50+
}
51+
52+
const cache = this.caches[index];
53+
return cache.del(key).then(() => {
54+
return this.__del(key, index + 1);
55+
});
56+
}
57+
}
58+
59+
exports.OnionCache = OnionCache;

lib/serializing.js

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
const { BaseCache } = require('./base');
2+
const debug = require('debug')('layer-cache:serializing');
3+
4+
const GET = 'get';
5+
const DELETE = 'del';
6+
7+
class Entry {
8+
constructor(requests) {
9+
this.requests = requests;
10+
}
11+
12+
push(request) {
13+
this.requests.push(request);
14+
}
15+
16+
detach() {
17+
const requests = this.requests;
18+
this.requests = [];
19+
return requests;
20+
}
21+
}
22+
23+
class Request {
24+
constructor(type, resolve, reject) {
25+
this.type = type;
26+
this.resolve = resolve;
27+
this.reject = reject;
28+
}
29+
}
30+
31+
class SerializingCache extends BaseCache {
32+
constructor(cache, fill) {
33+
super();
34+
35+
this.cache = cache;
36+
this.fill = fill;
37+
38+
this.entries = {};
39+
}
40+
41+
get(key) {
42+
return new Promise((resolve, reject) => {
43+
let entry = this.entries[key];
44+
if (!entry) {
45+
debug("Get '%s' started", key);
46+
this.entries[key] = new Entry([new Request(GET, resolve, reject)]);
47+
this.__run(key);
48+
} else {
49+
debug("Get '%s' queued (%d)", key, entry.requests.length);
50+
entry.requests.push(new Request(GET, resolve, reject));
51+
}
52+
});
53+
}
54+
55+
del(key) {
56+
return new Promise((resolve, reject) => {
57+
let entry = this.entries[key];
58+
if (!entry) {
59+
debug("Delete '%s' started", key);
60+
this.entries[key] = new Entry([new Request(DELETE, resolve, reject)]);
61+
this.__run(key);
62+
} else {
63+
debug("Delete '%s' queued", key);
64+
entry.requests.push(new Request(DELETE, resolve, reject));
65+
}
66+
});
67+
}
68+
69+
__run(key) {
70+
const entry = this.entries[key];
71+
72+
const requests = entry.detach();
73+
74+
const gets = requests.filter((request) => request.type === GET);
75+
const deletes = requests.filter((request) => request.type === DELETE);
76+
77+
let root = Promise.resolve();
78+
79+
if (deletes.length > 0) {
80+
root = root.then(() => {
81+
debug("Deleting '%s'", key);
82+
return this.cache.del(key).then(() => {
83+
debug("Delete '%s' successful", key);
84+
deletes.forEach(request => request.resolve());
85+
}).catch((err) => {
86+
debug("Delete '%s' failed", key);
87+
deletes.forEach(request => request.reject(err));
88+
});
89+
})
90+
}
91+
92+
if (gets.length > 0) {
93+
root = root.then(() => {
94+
return this.__query(key).then((result) => {
95+
debug("Get '%s' successful", key);
96+
gets.forEach(request => request.resolve(result));
97+
}).catch((err) => {
98+
debug("Get '%s' failed", key);
99+
gets.forEach(request => request.reject(err));
100+
});
101+
});
102+
}
103+
104+
root.catch(() => {}).then(() => {
105+
if (entry.requests.length > 0) {
106+
debug("Entry '%s' has new requests, restarting", key);
107+
this.__run(key);
108+
} else {
109+
debug("Entry '%s' is empty.");
110+
delete this.entries[key];
111+
}
112+
});
113+
}
114+
115+
__query(key) {
116+
return this.cache.get(key).then((result) => {
117+
if (result !== undefined) {
118+
return result;
119+
}
120+
121+
debug("Cache returned no result for '%s', filling...", key);
122+
return this.fill(key).then((result) => {
123+
debug("Fill '%s' successful", key);
124+
return this.cache.set(key, result).then(() => {
125+
debug("Storing '%s' successful");
126+
return result;
127+
}).catch((err) => {
128+
debug("Storing '%s' failed");
129+
throw err;
130+
});
131+
}).catch((err) => {
132+
debug("Fill '%s' failed", key);
133+
throw err;
134+
});
135+
})
136+
}
137+
}
138+
139+
exports.SerializingCache = SerializingCache;

package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "layer-cache",
3+
"version": "1.0.0",
4+
"main": "./index",
5+
"private": true,
6+
"author": "[email protected]",
7+
"dependencies": {
8+
"lru-cache": "^4.1.2"
9+
},
10+
"devDependencies": {
11+
"assert": "^1.4.1",
12+
"debug": "== 2.6.4",
13+
"mocha": "^5.0.4"
14+
},
15+
"peerDependencies": {
16+
"debug": "2.x"
17+
},
18+
"scripts": {
19+
"test": "mocha"
20+
}
21+
}

test/memory.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const { MemoryCache } = require('../lib/memory');
2+
3+
const assert = require('assert');
4+
5+
describe('MemoryCache', () => {
6+
it('should return previously set data', function () {
7+
const cache = new MemoryCache();
8+
const testKey = 'key';
9+
const testValue = 'value';
10+
11+
return cache.set(testKey, testValue).then(function () {
12+
return cache.get(testKey).then((result) => {
13+
assert(result === testValue);
14+
});
15+
});
16+
});
17+
18+
it('should not return expired data', function () {
19+
const cache = new MemoryCache({ maxAge: 5 });
20+
const testKey = 'key';
21+
const testValue = 'value';
22+
23+
return cache.set(testKey, testValue).then(function () {
24+
return new Promise(function (resolve) {
25+
setTimeout(resolve, 10);
26+
}).then(function () {
27+
return cache.get(testKey).then((result) => {
28+
assert(result === undefined);
29+
});
30+
});
31+
});
32+
});
33+
34+
it('should not return expunged data', function () {
35+
const cache = new MemoryCache({ max: 1 });
36+
const testKey1 = 'key1';
37+
const testKey2 = 'key2';
38+
const testValue = 'value';
39+
40+
return cache.set(testKey1, testValue).then(function () {
41+
return cache.set(testKey2, testValue);
42+
}).then(function () {
43+
return cache.get(testKey1).then((result) => {
44+
assert(result === undefined);
45+
});
46+
}).then(function () {
47+
return cache.get(testKey2).then((result) => {
48+
assert(result === testValue);
49+
});
50+
});
51+
})
52+
});

0 commit comments

Comments
 (0)