Skip to content

Commit 160145b

Browse files
committed
lib: add cache utility
1 parent 3dafebf commit 160145b

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ tmp
66
coverage.lcov
77
tmp-*
88
.eslintcache
9+
.ncu

lib/cache.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
const fs = require('fs');
5+
const { writeJson, readJson, writeFile, readFile } = require('./file');
6+
7+
function isAsync(fn) {
8+
return fn[Symbol.toStringTag] === 'AsyncFunction';
9+
}
10+
11+
class Cache {
12+
constructor(dir) {
13+
this.dir = dir || path.join(__dirname, '..', '.ncu', 'cache');
14+
this.originals = {};
15+
this.disabled = true;
16+
}
17+
18+
disable() {
19+
this.disabled = true;
20+
}
21+
22+
enable() {
23+
this.disabled = false;
24+
}
25+
26+
getFilename(key, ext) {
27+
return path.join(this.dir, key) + ext;
28+
}
29+
30+
has(key, ext) {
31+
if (this.disabled) {
32+
return false;
33+
}
34+
35+
return fs.existsSync(this.getFilename(key, ext));
36+
}
37+
38+
get(key, ext) {
39+
if (!this.has(key, ext)) {
40+
return undefined;
41+
}
42+
if (ext === '.json') {
43+
return readJson(this.getFilename(key, ext));
44+
} else {
45+
return readFile(this.getFilename(key, ext));
46+
}
47+
}
48+
49+
write(key, ext, content) {
50+
if (this.disabled) {
51+
return;
52+
}
53+
const filename = this.getFilename(key, ext);
54+
if (ext === '.json') {
55+
return writeJson(filename, content);
56+
} else {
57+
return writeFile(filename, content);
58+
}
59+
}
60+
61+
wrapAsync(original, identity) {
62+
const cache = this;
63+
return async function(...args) {
64+
const { key, ext } = identity.call(this, ...args);
65+
const cached = cache.get(key, ext);
66+
if (cached) {
67+
return cached;
68+
}
69+
const result = await original.call(this, ...args);
70+
cache.write(key, ext, result);
71+
return result;
72+
};
73+
}
74+
75+
wrapNormal(original, identity) {
76+
const cache = this;
77+
return function(...args) {
78+
const { key, ext } = identity.call(this, ...args);
79+
const cached = cache.get(key, ext);
80+
if (cached) {
81+
return cached;
82+
}
83+
const result = original.call(this, ...args);
84+
cache.write(key, ext, result);
85+
return result;
86+
};
87+
}
88+
89+
wrap(Class, identities) {
90+
for (let method of Object.keys(identities)) {
91+
const original = Class.prototype[method];
92+
const identity = identities[method];
93+
this.originals[method] = original;
94+
if (isAsync(original)) {
95+
Class.prototype[method] = this.wrapAsync(original, identity);
96+
} else {
97+
Class.prototype[method] = this.wrapNormal(original, identity);
98+
}
99+
}
100+
}
101+
}
102+
103+
module.exports = Cache;

test/common.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
const path = require('path');
4+
const rimraf = require('rimraf');
5+
const mkdirp = require('mkdirp');
6+
const fs = require('fs');
7+
8+
exports.tmpdir = {
9+
get path() {
10+
return path.join(__dirname, 'tmp');
11+
},
12+
refresh() {
13+
rimraf.sync(this.path);
14+
mkdirp.sync(this.path);
15+
}
16+
};
17+
18+
exports.copyShallow = function(src, dest) {
19+
mkdirp.sync(dest);
20+
const list = fs.readdirSync(src);
21+
for (const file of list) {
22+
fs.copyFileSync(path.join(src, file), path.join(dest, file));
23+
}
24+
};

test/unit/cache.test.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use strict';
2+
3+
const Cache = require('../../lib/cache');
4+
const { tmpdir } = require('../common');
5+
const path = require('path');
6+
const fs = require('fs');
7+
const assert = require('assert');
8+
9+
describe('Cache', () => {
10+
const syncResult = 'content in sync';
11+
const asyncResult = {
12+
results: 'content in async.json'
13+
};
14+
15+
class CachedClass {
16+
constructor(foo) {
17+
this.foo = foo;
18+
this.sync = 0;
19+
this.async = 0;
20+
}
21+
22+
cachedSyncMethod(...args) {
23+
this.sync++;
24+
return syncResult;
25+
}
26+
27+
async cachedAsyncMethod(...args) {
28+
this.async++;
29+
const p = Promise.resolve(asyncResult);
30+
const result = await p; // make sure it's async
31+
return result;
32+
}
33+
34+
getCacheKey(prefix, ...args) {
35+
return `${prefix}-${args.join('-')}-${this.foo}`;
36+
}
37+
}
38+
39+
tmpdir.refresh();
40+
const cache = new Cache(tmpdir.path);
41+
cache.wrap(CachedClass, {
42+
cachedSyncMethod(...args) {
43+
return { key: this.getCacheKey('sync', ...args), ext: '.txt' };
44+
},
45+
cachedAsyncMethod(...args) {
46+
return { key: this.getCacheKey('async', ...args), ext: '.json' };
47+
}
48+
});
49+
50+
it('should cache sync results', () => {
51+
tmpdir.refresh();
52+
cache.enable();
53+
const expected = syncResult;
54+
const instance = new CachedClass('foo');
55+
let actual = instance.cachedSyncMethod('test');
56+
assert.strictEqual(instance.sync, 1);
57+
assert.strictEqual(actual, expected);
58+
59+
let syncCache = path.join(tmpdir.path, 'sync-test-foo.txt');
60+
let cached = fs.readFileSync(syncCache, 'utf8');
61+
assert.strictEqual(cached, expected);
62+
63+
// Call it again
64+
actual = instance.cachedSyncMethod('test');
65+
assert.strictEqual(instance.sync, 1);
66+
assert.strictEqual(actual, expected);
67+
68+
syncCache = path.join(tmpdir.path, 'sync-test-foo.txt');
69+
cached = fs.readFileSync(syncCache, 'utf8');
70+
assert.strictEqual(cached, expected);
71+
});
72+
73+
it('should cache async results', async() => {
74+
tmpdir.refresh();
75+
cache.enable();
76+
const expected = Object.assign({}, asyncResult);
77+
const instance = new CachedClass('foo');
78+
let actual = await instance.cachedAsyncMethod('test');
79+
assert.strictEqual(instance.async, 1);
80+
assert.deepStrictEqual(actual, expected);
81+
82+
let asyncCache = path.join(tmpdir.path, 'async-test-foo.json');
83+
let cached = JSON.parse(fs.readFileSync(asyncCache, 'utf8'));
84+
assert.deepStrictEqual(cached, expected);
85+
86+
// Call it again
87+
actual = await instance.cachedAsyncMethod('test');
88+
assert.strictEqual(instance.async, 1);
89+
assert.deepStrictEqual(actual, expected);
90+
91+
asyncCache = path.join(tmpdir.path, 'async-test-foo.json');
92+
cached = JSON.parse(fs.readFileSync(asyncCache, 'utf8'));
93+
assert.deepStrictEqual(cached, expected);
94+
});
95+
96+
it('should not cache if disabled', async() => {
97+
tmpdir.refresh();
98+
cache.disable();
99+
const expected = Object.assign({}, asyncResult);
100+
const instance = new CachedClass('foo');
101+
let actual = await instance.cachedAsyncMethod('test');
102+
assert.strictEqual(instance.async, 1);
103+
assert.deepStrictEqual(actual, expected);
104+
105+
let list = fs.readdirSync(tmpdir.path);
106+
assert.deepStrictEqual(list, []);
107+
108+
// Call it again
109+
actual = await instance.cachedAsyncMethod('test');
110+
assert.strictEqual(instance.async, 2);
111+
assert.deepStrictEqual(actual, expected);
112+
113+
list = fs.readdirSync(tmpdir.path);
114+
assert.deepStrictEqual(list, []);
115+
});
116+
});

0 commit comments

Comments
 (0)