151 lines
4.2 KiB
JavaScript
151 lines
4.2 KiB
JavaScript
|
const path = require('path')
|
||
|
const arrayify = require('array-back')
|
||
|
const fs = require('fs-then-native')
|
||
|
|
||
|
/**
|
||
|
* A memoisation solution intended to cache the output of expensive operations, speeding up future invocations with the same input.
|
||
|
* @module cache-point
|
||
|
* @example
|
||
|
* const Cache = require('cache-point')
|
||
|
* const cache = new Cache({ dir: 'tmp/example' })
|
||
|
*
|
||
|
* // The first invocation will take 3s, the rest instantaneous.
|
||
|
* // outputs: 'result'
|
||
|
* getData('some input')
|
||
|
* .then(console.log)
|
||
|
*
|
||
|
* // check the cache for output generated with this input.
|
||
|
* // cache.read() will resolve on hit, reject on miss.
|
||
|
* function getData (input) {
|
||
|
* return cache
|
||
|
* .read(input)
|
||
|
* .catch(() => expensiveOperation(input))
|
||
|
* }
|
||
|
*
|
||
|
* // The expensive operation we're aiming to avoid,
|
||
|
* // (3 second cost per invocation)
|
||
|
* function expensiveOperation (input) {
|
||
|
* return new Promise((resolve, reject) => {
|
||
|
* setTimeout(() => {
|
||
|
* const output = 'result'
|
||
|
* cache.write(input, output)
|
||
|
* resolve(output)
|
||
|
* }, 3000)
|
||
|
* })
|
||
|
* }
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @alias module:cache-point
|
||
|
* @typicalname cache
|
||
|
*/
|
||
|
class Cache {
|
||
|
/**
|
||
|
* @param [options] {object}
|
||
|
* @param [options.dir] {string}
|
||
|
*/
|
||
|
constructor (options) {
|
||
|
options = options || {}
|
||
|
if (!options.dir) {
|
||
|
var os = require('os')
|
||
|
options.dir = path.resolve(os.tmpdir(), 'cachePoint')
|
||
|
}
|
||
|
/**
|
||
|
* Current cache directory. Can be changed at any time.
|
||
|
* @type {string}
|
||
|
*/
|
||
|
this.dir = options.dir
|
||
|
}
|
||
|
|
||
|
get dir () {
|
||
|
return this._dir
|
||
|
}
|
||
|
set dir (val) {
|
||
|
this._dir = val
|
||
|
const mkdirp = require('mkdirp2')
|
||
|
mkdirp.sync(this.dir)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A cache hit resolves with the stored value, a miss rejects.
|
||
|
* @param {*} - One or more values to uniquely identify the data. Can be any value, or an array of values of any type.
|
||
|
* @returns {Promise}
|
||
|
*/
|
||
|
read (keys) {
|
||
|
const blobPath = path.resolve(this._dir, this.getChecksum(keys))
|
||
|
return fs.readFile(blobPath).then(JSON.parse)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A cache hit returns the stored value, a miss returns `null`.
|
||
|
* @param {*} - One or more values to uniquely identify the data. Can be any value, or an array of values of any type.
|
||
|
* @returns {string?}
|
||
|
*/
|
||
|
readSync (keys) {
|
||
|
const blobPath = path.resolve(this._dir, this.getChecksum(keys))
|
||
|
try {
|
||
|
const data = fs.readFileSync(blobPath, 'utf8')
|
||
|
return JSON.parse(data)
|
||
|
} catch (err) {
|
||
|
return null
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Write some data to the cache. Returns a promise which resolves when the write is complete.
|
||
|
* @param {*} - One or more values to index the data, e.g. a request object or set of function args.
|
||
|
* @param {*} - the data to store
|
||
|
* @returns {Promise}
|
||
|
*/
|
||
|
write (keys, content) {
|
||
|
const blobPath = path.resolve(this._dir, this.getChecksum(keys))
|
||
|
return fs.writeFile(blobPath, JSON.stringify(content))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Write some data to the cache with a key.
|
||
|
* @param {*} - One or more values to index the data, e.g. a request object or set of function args.
|
||
|
* @param {*} - the data to store
|
||
|
*/
|
||
|
writeSync (keys, content) {
|
||
|
const blobPath = path.resolve(this._dir, this.getChecksum(keys))
|
||
|
fs.writeFileSync(blobPath, JSON.stringify(content))
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used internally to convert a key value into a hex checksum. Override if for some reason you need a different hashing strategy.
|
||
|
* @param {*} - One or more values to index the data, e.g. a request object or set of function args.
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
getChecksum (keys) {
|
||
|
const crypto = require('crypto')
|
||
|
const hash = crypto.createHash('sha1')
|
||
|
arrayify(keys).forEach(key => hash.update(JSON.stringify(key)))
|
||
|
return hash.digest('hex')
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears the cache. Returns a promise which resolves once the cache is clear.
|
||
|
* @returns {Promise}
|
||
|
*/
|
||
|
clear () {
|
||
|
return fs.readdir(this._dir)
|
||
|
.then(files => {
|
||
|
const promises = files.map(file => fs.unlink(path.resolve(this._dir, file)))
|
||
|
return Promise.all(promises)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clears and removes the cache directory. Returns a promise which resolves once the remove is complete.
|
||
|
* @returns {Promise}
|
||
|
*/
|
||
|
remove () {
|
||
|
return this.clear().then(() => {
|
||
|
return fs.rmdir(this._dir)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Cache
|