Skip to content

Commit

Permalink
Merge pull request #63 from Automattic/refactor/cache
Browse files Browse the repository at this point in the history
refactor: cache module
  • Loading branch information
sjinks authored Apr 20, 2024
2 parents ab3f110 + 9d18a28 commit 109d936
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 129 deletions.
86 changes: 38 additions & 48 deletions lib/cache.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
'use strict';

// Modules
const _ = require('lodash');
const fs = require('fs');
const jsonfile = require('jsonfile');
const {mkdirSync, unlinkSync} = require('node:fs');
const {readFileSync, writeFileSync} = require('jsonfile');
const Log = require('./logger');
const NodeCache = require('node-cache');
const os = require('os');
const path = require('path');
const {tmpdir} = require('node:os');
const {join} = require('node:path');

/*
/**
* Creates a new Cache instance.
*
* @property {Log} log A log instance
* @property {string} cacheDir The directory to store cache files in
*/
class Cache extends NodeCache {
constructor({log = new Log(), cacheDir = path.join(os.tmpdir(), '.cache')} = {}) {
module.exports = class Cache extends NodeCache {
/**
* @param {Object} [opts] Options to pass into the cache
* @param {Log} [opts.log] A log instance
* @param {string} [opts.cacheDir] The directory to store cache files in
*/
constructor({log = new Log(), cacheDir = join(tmpdir(), '.cache')} = {}) {
// Get the nodecache opts
super();
// Set some things
this.log = log;
this.cacheDir = cacheDir;
// Ensure the cache dir exists
fs.mkdirSync(this.cacheDir, {recursive: true});
mkdirSync(this.cacheDir, {recursive: true});
};

/**
* Sets an item in the cache
*
* @since 3.0.0
* @alias lando.cache.set
* @param {String} key The name of the key to store the data with.
* @param {Any} data The data to store in the cache.
* @param {string} key The name of the key to store the data with.
* @param {any} data The data to store in the cache.
* @param {Object} [opts] Options to pass into the cache
* @param {Boolean} [opts.persist=false] Whether this cache data should persist between processes. Eg in a file instead of memory
* @param {Integer} [opts.ttl=0] Seconds the cache should live. 0 mean forever.
* @param {boolean} [opts.persist=false] Whether this cache data should persist between processes. Eg in a file instead of memory
* @param {number} [opts.ttl=0] Seconds the cache should live. 0 mean forever.
* @example
* // Add a string to the cache
* lando.cache.set('mykey', 'mystring');
Expand All @@ -46,41 +53,44 @@ class Cache extends NodeCache {
set(key, data, {persist = false, ttl = 0} = {}) {
// Unsafe cache key patterns
const patterns = {
controlRe: /[\x00-\x1f\x80-\x9f]/g,
illegalRe: /[\/\?<>\\:\*\|":]/g,
controlRe: /[\x00-\x1f\x80-\x9f]/g, // NOSONAR
illegalRe: /[/?<>\\*|":]/g, // NOSONAR
reservedRe: /^\.+$/,
windowsReservedRe: /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i,
windowsTrailingRe: /[\. ]+$/,
windowsReservedRe: /^(con|prn|aux|nul|com\d|lpt\d)(\..*)?$/i,
windowsTrailingRe: /[. ]+$/,
};
_.map(patterns, pattern => {
if (key.match(pattern)) throw new Error(`Invalid cache key: ${key}`);
});

if (Object.values(patterns).some(pattern => pattern.test(key))) {
throw new Error(`Invalid cache key: ${key}`);
}

// Try to set cache
if (this.__set(key, data, ttl)) {
if (super.set(key, data, ttl)) {
this.log.debug('Cached %j with key %s for %j', data, key, {persist, ttl});
} else {
this.log.debug('Failed to cache %j with key %s', data, key);
}

// And add to file if we have persistence
if (persist) jsonfile.writeFileSync(path.join(this.cacheDir, key), data);
if (persist) {
writeFileSync(join(this.cacheDir, key), data);
}
};

/**
* Gets an item in the cache
*
* @since 3.0.0
* @alias lando.cache.get
* @param {String} key The name of the key to retrieve the data.
* @return {Any} The data stored in the cache if applicable.
* @param {string} key The name of the key to retrieve the data.
* @return {any} The data stored in the cache if applicable.
* @example
* // Get the data stored with key mykey
* const data = lando.cache.get('mykey');
*/
get(key) {
// Get from cache
const memResult = this.__get(key);
const memResult = super.get(key);

// Return result if its in memcache
if (memResult) {
Expand All @@ -89,7 +99,7 @@ class Cache extends NodeCache {
} else {
try {
this.log.debug('Trying to retrieve from file cache with key %s', key);
return jsonfile.readFileSync(path.join(this.cacheDir, key));
return readFileSync(join(this.cacheDir, key));
} catch (e) {
this.log.debug('File cache miss with key %s', key);
}
Expand All @@ -108,34 +118,14 @@ class Cache extends NodeCache {
*/
remove(key) {
// Try to get cache
if (this.__del(key)) this.log.debug('Removed key %s from memcache.', key);
if (super.del(key)) this.log.debug('Removed key %s from memcache.', key);
else this.log.debug('Failed to remove key %s from memcache.', key);

// Also remove file if applicable
try {
fs.unlinkSync(path.join(this.cacheDir, key));
unlinkSync(join(this.cacheDir, key));
} catch (e) {
this.log.debug('No file cache with key %s', key);
}
};
};

/*
* Stores the old get method.
*/
Cache.prototype.__get = NodeCache.prototype.get;

/*
* Stores the old set method.
*/
Cache.prototype.__set = NodeCache.prototype.set;

/*
* Stores the old del method.
*/
Cache.prototype.__del = NodeCache.prototype.del;

/*
* Return the class
*/
module.exports = Cache;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@
"inquirer-autocomplete-prompt": "^1.0.1",
"ip": "^1.1.9",
"js-yaml": "^4.1.0",
"jsonfile": "^2.4.0",
"jsonfile": "^6.1.0",
"lodash": "^4.17.21",
"node-cache": "^4.1.1",
"node-cache": "^5.1.2",
"object-hash": "^1.1.8",
"semver": "^7.3.8",
"shelljs": "^0.8.4",
Expand Down
66 changes: 0 additions & 66 deletions test/cache.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@

'use strict';

const _ = require('lodash');
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
const filesystem = require('mock-fs');
const fs = require('fs');
const NodeCache = require('node-cache');
chai.should();

const Cache = require('./../lib/cache');
Expand All @@ -24,7 +22,6 @@ describe('cache', () => {
cache.should.be.an('object').with.property('options');
cache.options.should.have.property('stdTTL', 0);
cache.options.should.have.property('checkperiod', 600);
cache.options.should.have.property('errorOnMissing', false);
cache.options.should.have.property('useClones', true);
cache.options.should.have.property('deleteOnExpire', true);
});
Expand Down Expand Up @@ -55,51 +52,6 @@ describe('cache', () => {
});
});

describe('#__get', () => {
it('should be the same as new NodeCache().get', () => {
filesystem();

const cache = new Cache();
cache.set('yyz', 'amazing');

const nCache = new NodeCache();
nCache.set('yyz', 'amazing');

cache.__get('yyz').should.eql(nCache.get('yyz'));

filesystem.restore();
});
});

describe('#__set', () => {
it('should be the same as new NodeCache().set', () => {
filesystem();

const cache = new Cache();
const nCache = new NodeCache();
cache.__set('yyz', 'amazing').should.eql(nCache.set('yyz', 'amazing'));

filesystem.restore();
});
});

describe('#__del', () => {
it('should be the same as new NodeCache().del', () => {
filesystem();

const cache = new Cache();
const nCache = new NodeCache();
cache.__set('yyz', 'amazing');
const returnone = cache.__del('yyz');
nCache.set('yyz', 'amazing');
const returntwo = nCache.del('yyz');

returnone.should.eql(returntwo);

filesystem.restore();
});
});

describe('#set', () => {
it('should set a cached key in memory', () => {
filesystem();
Expand All @@ -111,15 +63,6 @@ describe('cache', () => {
filesystem.restore();
});

it('should log a failure when key cannot be cached in memory', () => {
const cache = new Cache({log: {debug: sinon.spy()}});
sinon.stub(cache, '__set').returns(false);
cache.set('test', 'thing');
const call = cache.log.debug.getCall(0);
expect(_.includes(call.args[0], 'Failed')).to.equal(true);
cache.log.debug.callCount.should.equal(1);
});

it('should remove a cached key in memory after ttl has expired', () => {
filesystem();
const clock = sinon.useFakeTimers();
Expand Down Expand Up @@ -214,14 +157,5 @@ describe('cache', () => {
fs.existsSync('/tmp/cache/subdivisions').should.be.false;
filesystem.restore();
});

it('should log a failure when key cannot be removed from memory', () => {
const cache = new Cache({log: {debug: sinon.spy()}});
sinon.stub(cache, '__del').returns(false);
cache.remove('test');
const call = cache.log.debug.getCall(0);
expect(_.includes(call.args[0], 'Failed')).to.equal(true);
cache.log.debug.callCount.should.equal(2);
});
});
});
18 changes: 5 additions & 13 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2011,21 +2011,14 @@ json5@^2.2.3:
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==

jsonfile@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug=
optionalDependencies:
graceful-fs "^4.1.6"

jsonfile@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
optionalDependencies:
graceful-fs "^4.1.6"

jsonfile@^6.0.1:
jsonfile@^6.0.1, jsonfile@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
Expand Down Expand Up @@ -2386,13 +2379,12 @@ node-abi@^3.3.0:
dependencies:
semver "^7.3.5"

node-cache@^4.1.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.2.1.tgz#efd8474dee4edec4138cdded580f5516500f7334"
integrity sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==
node-cache@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
dependencies:
clone "2.x"
lodash "^4.17.15"

node-fetch@^2.6.6:
version "2.7.0"
Expand Down

0 comments on commit 109d936

Please sign in to comment.