memoize-fs
Advanced tools
Comparing version 2.0.0 to 2.1.0
@@ -0,1 +1,4 @@ | ||
### 2.1.0 (2020-02-28) | ||
* feat: support custom serialize & deserialize through options | ||
### 2.0.0 (2020-02-03) | ||
@@ -2,0 +5,0 @@ |
108
index.js
@@ -1,7 +0,7 @@ | ||
const mkdirp = require('mkdirp') | ||
const fs = require('fs') | ||
'use strict' | ||
const path = require('path') | ||
const rmdir = require('rimraf') | ||
const crypto = require('crypto') | ||
const meriyah = require('meriyah') | ||
const fs = require('fs-extra') | ||
@@ -11,2 +11,7 @@ module.exports = buildMemoizer | ||
const serializer = { | ||
serialize, | ||
deserialize | ||
} | ||
function serialize (val) { | ||
@@ -30,15 +35,22 @@ const circRefColl = [] | ||
function deserialize (str) { | ||
return JSON.parse(str).data | ||
} | ||
function getCacheFilePath (fn, args, opt) { | ||
const salt = opt.salt || '' | ||
const options = { ...serializer, ...opt } | ||
const salt = options.salt || '' | ||
let fnStr = '' | ||
if (!opt.noBody) { | ||
if (!options.noBody) { | ||
fnStr = String(fn) | ||
if (opt.astBody) { | ||
if (options.astBody) { | ||
fnStr = meriyah.parse(fnStr, { jsx: true, next: true }) | ||
} | ||
fnStr = opt.astBody ? JSON.stringify(fnStr) : fnStr | ||
fnStr = options.astBody ? JSON.stringify(fnStr) : fnStr | ||
} | ||
const argsStr = serialize(args) | ||
const argsStr = options.serialize(args) | ||
const hash = crypto.createHash('md5').update(fnStr + argsStr + salt).digest('hex') | ||
return path.join(opt.cachePath, opt.cacheId, hash) | ||
return path.join(options.cachePath, options.cacheId, hash) | ||
} | ||
@@ -50,3 +62,3 @@ | ||
// check args | ||
if (typeof options !== 'object') { | ||
if (!options || (options && typeof options !== 'object')) { | ||
throw new Error('options of type object expected') | ||
@@ -57,9 +69,37 @@ } | ||
} | ||
options = { ...serializer, ...options } | ||
checkOptions(options) | ||
function checkOptions (opts) { | ||
if (opts.salt && typeof opts.salt !== 'string') { | ||
throw new TypeError('salt option of type string expected, got: ' + typeof opts.salt) | ||
} | ||
if (opts.cacheId && typeof opts.cacheId !== 'string') { | ||
throw new TypeError('cacheId option of type string expected, got: ' + typeof opts.cacheId) | ||
} | ||
if (opts.maxAge && typeof opts.maxAge !== 'number') { | ||
throw new TypeError('maxAge option of type number bigger zero expected') | ||
} | ||
if (opts.serialize && typeof opts.serialize !== 'function') { | ||
throw new TypeError('serialize option of type function expected') | ||
} | ||
if (opts.deserialize && typeof opts.deserialize !== 'function') { | ||
throw new TypeError('deserialize option of type function expected') | ||
} | ||
} | ||
// check for existing cache folder, if not found, create folder, then resolve | ||
function initCache (cachePath) { | ||
function initCache (cachePath, opts) { | ||
return new Promise(function (resolve, reject) { | ||
return mkdirp(cachePath).then(() => { | ||
resolve() | ||
}).catch(reject) | ||
return fs.ensureDir(cachePath, { recursive: true }) | ||
.then(() => { | ||
resolve() | ||
}) | ||
.catch((err) => { | ||
if (err && err.code === 'EEXIST' && opts.throwError === false) { | ||
resolve() | ||
return | ||
} | ||
reject(err) | ||
}) | ||
}) | ||
@@ -69,14 +109,2 @@ } | ||
function memoizeFn (fn, opt) { | ||
function checkOptions (optExt) { | ||
if (optExt.salt && typeof optExt.salt !== 'string') { | ||
throw new Error('salt option of type string expected, got \'' + typeof optExt.salt + '\'') | ||
} | ||
if (optExt.cacheId && typeof optExt.cacheId !== 'string') { | ||
throw new Error('cacheId option of type string expected, got \'' + typeof optExt.cacheId + '\'') | ||
} | ||
if (optExt.maxAge && typeof optExt.maxAge !== 'number') { | ||
throw new Error('maxAge option of type number bigger zero expected') | ||
} | ||
} | ||
if (opt && typeof opt !== 'object') { | ||
@@ -86,11 +114,9 @@ throw new Error('opt of type object expected, got \'' + typeof opt + '\'') | ||
const optExt = opt || {} | ||
if (typeof fn !== 'function') { | ||
throw new Error('fn of type function expected') | ||
} | ||
const optExt = { cacheId: './', ...options, ...opt } | ||
checkOptions(optExt) | ||
optExt.cacheId = optExt.cacheId || './' | ||
function resolveWithMemFn () { | ||
@@ -121,3 +147,3 @@ return new Promise(function (resolve) { | ||
resultObj = { data: r } | ||
resultString = serialize(resultObj) | ||
resultString = optExt.serialize(resultObj) | ||
} else { | ||
@@ -163,3 +189,3 @@ resultString = '{"data":' + r + '}' | ||
} | ||
if (result && result.then && typeof result.then === 'function') { | ||
if (result && typeof result.then === 'function') { | ||
// result is a promise instance | ||
@@ -199,3 +225,5 @@ return result.then(function (retObj) { | ||
try { | ||
return JSON.parse(resultString).data // will fail on NaN | ||
const deserializedValue = optExt.deserialize(resultString) | ||
return deserializedValue | ||
} catch (e) { | ||
@@ -240,3 +268,3 @@ return undefined | ||
return initCache(path.join(options.cachePath, optExt.cacheId)).then(resolveWithMemFn) | ||
return initCache(path.join(options.cachePath, optExt.cacheId), optExt).then(resolveWithMemFn) | ||
} | ||
@@ -250,9 +278,5 @@ | ||
const cachPath = cacheId ? path.join(options.cachePath, cacheId) : options.cachePath | ||
rmdir(cachPath, function (err) { | ||
if (err) { | ||
reject(err) | ||
} else { | ||
resolve() | ||
} | ||
}) | ||
fs.remove(cachPath) | ||
.then(resolve) | ||
.catch(reject) | ||
} | ||
@@ -263,3 +287,3 @@ }) | ||
function getCacheFilePathBound (fn, args, opt) { | ||
return getCacheFilePath(fn, args, Object.assign({}, opt, { cachePath: options.cachePath })) | ||
return getCacheFilePath(fn, args, { ...options, ...opt, cachePath: options.cachePath }) | ||
} | ||
@@ -266,0 +290,0 @@ |
{ | ||
"name": "memoize-fs", | ||
"version": "2.0.0", | ||
"description": "memoize/cache in file system solution for Node.js", | ||
"version": "2.1.0", | ||
"description": "Node.js solution for memoizing/caching a function and its return state into the file system", | ||
"author": "Boris Diakur <contact@borisdiakur.com> (https://github.com/borisdiakur)", | ||
"scripts": { | ||
"test": "npm run eslint && mocha test", | ||
"eslint": "eslint .", | ||
"mocha": "mocha --reporter nyan test", | ||
"istanbul": "rm -rf coverage && ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report html && open coverage/memoize-fs/index.js.html", | ||
"coveralls": "rm -rf coverage && ./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" | ||
"lint": "eslint .", | ||
"pretest": "npm run lint", | ||
"test": "nyc mocha", | ||
"mocha": "mocha -R nyan", | ||
"coverage": "nyc report --reporter text-lcov | coveralls", | ||
"cov": "open-cli coverage/lcov-report/index.html" | ||
}, | ||
@@ -22,9 +23,8 @@ "homepage": "https://github.com/borisdiakur/memoize-fs", | ||
"engines": { | ||
"node": ">= 10.0.0", | ||
"node": ">= 10.13.0", | ||
"npm": ">= 6.0.0" | ||
}, | ||
"dependencies": { | ||
"meriyah": "^1.9.7", | ||
"mkdirp": "^1.0.3", | ||
"rimraf": "^3.0.1" | ||
"fs-extra": "^8.1.0", | ||
"meriyah": "^1.9.7" | ||
}, | ||
@@ -39,8 +39,14 @@ "devDependencies": { | ||
"eslint-plugin-standard": "^4.0.1", | ||
"istanbul": "^0.4.5", | ||
"mocha": "^7.0.1", | ||
"mocha-lcov-reporter": "1.3.0" | ||
"mocha": "^7.1.0", | ||
"nyc": "^15.0.0", | ||
"open-cli": "^5.0.0", | ||
"serialize-javascript": "^3.0.0" | ||
}, | ||
"private": false, | ||
"main": "index.js", | ||
"typings": "index.d.ts", | ||
"files": [ | ||
"index.js", | ||
"index.d.ts" | ||
], | ||
"keywords": [ | ||
@@ -62,3 +68,16 @@ "memoize", | ||
"license": "MIT", | ||
"readmeFilename": "README.md" | ||
"nyc": { | ||
"exclude": [ | ||
"test" | ||
], | ||
"reporter": [ | ||
"text", | ||
"lcov" | ||
], | ||
"check-coverage": true, | ||
"branches": 80, | ||
"lines": 80, | ||
"functions": 80, | ||
"statements": 80 | ||
} | ||
} |
209
README.md
# memoize-fs | ||
memoize/cache in file system solution for Node.js | ||
Node.js solution for memoizing/caching a function and its return state into the file system | ||
@@ -35,21 +35,61 @@ [![Build Status](https://travis-ci.org/borisdiakur/memoize-fs.svg?branch=master)](https://travis-ci.org/borisdiakur/memoize-fs) | ||
```javascript | ||
var cachePath = require('path').join(__dirname, '..', 'cache'), | ||
memoize = require('memoize-fs')({ cachePath: cachePath }), | ||
fun = function (a, b) { return a + b; }; | ||
```js | ||
const assert = require('assert') | ||
const memoizeFs = require('memoize-fs') | ||
memoize.fn(fun).then(function (memFn) { | ||
memFn(1, 2).then(function (result) { | ||
assert.strictEqual(result, 3); | ||
return memFn(1, 2); // cache hit | ||
}).then(function (result) { | ||
assert.strictEqual(result, 3); | ||
}).catch( /* handle error */ ); | ||
}).catch( /* handle error */ ); | ||
const memoizer = memoizeFs({ cachePath: './some-cache' }) | ||
console.log(memoizer) | ||
// => { | ||
// fn: [Function: fn], | ||
// getCacheFilePath: [Function: getCacheFilePathBound], | ||
// invalidate: [Function: invalidateCache] | ||
// } | ||
async function main () { | ||
let idx = 0 | ||
const func = function foo (a, b) { | ||
idx += a + b | ||
return idx | ||
} | ||
const memoizedFn = await memoizer.fn(func) | ||
const resultOne = await memoizedFn(1, 2) | ||
assert.strictEqual(resultOne, 3) | ||
assert.strictEqual(idx, 3) | ||
const resultTwo = await memoizedFn(1, 2) // cache hit | ||
assert.strictEqual(resultTwo, 3) | ||
assert.strictEqual(idx, 3) | ||
} | ||
main().catch(console.error) | ||
``` | ||
__Note that a result of a memoized function is always a [Promise](http://www.html5rocks.com/en/tutorials/es6/promises/) instance!__ | ||
_**NOTE:** that memoized function is always an async function and | ||
the result of it is a Promise (if not `await`-ed as seen in above example)!_ | ||
### Memoizing asynchronous functions | ||
- [Learn more about Promises](https://javascript.info/promise-basics) | ||
- [Learn more about async/await](https://javascript.info/async-await) | ||
### Signature | ||
See [Types](#types) and [Options](#options) sections for more info. | ||
```js | ||
const memoizer = memoizeFs(MemoizeOptions) | ||
console.log(memoizer) | ||
// => { | ||
// fn: [Function: fn], | ||
// getCacheFilePath: [Function: getCacheFilePathBound], | ||
// invalidate: [Function: invalidateCache] | ||
// } | ||
const memoizedFn = memoizer.fn(FunctionToMemoize, Options?) | ||
``` | ||
## Memoizing asynchronous functions | ||
memoize-fs assumes a function asynchronous if the last argument it accepts is of type `function` and that function itself accepts at least one argument. | ||
@@ -59,3 +99,3 @@ So basically you don't have to do anything differently than when memoizing synchronous functions. Just make sure the above condition is fulfilled. | ||
```javascript | ||
```js | ||
var funAsync = function (a, b, cb) { | ||
@@ -76,3 +116,3 @@ setTimeout(function () { | ||
### Memoizing promisified functions | ||
## Memoizing promisified functions | ||
@@ -85,3 +125,3 @@ You can also memoize a promisified function. memoize-fs assumes a function promisified if its result is _thenable_ | ||
```javascript | ||
```js | ||
var funPromisified = function (a, b) { | ||
@@ -103,12 +143,47 @@ return new Promise(function (resolve, reject) { | ||
### Options | ||
## Types | ||
```ts | ||
export interface Options { | ||
cacheId?: string; | ||
salt?: string; | ||
maxAge?: number; | ||
force?: boolean; | ||
astBody?: boolean; | ||
noBody?: boolean; | ||
serialize?: (val?: any) => string; | ||
deserialize?: (val?: string) => any; | ||
} | ||
export type MemoizeOptions = Options & { cachePath: string }; | ||
export type FnToMemoize = (...args: any[]) => any; | ||
export interface Memoizer { | ||
fn: (fnToMemoize: FunctionToMemoize, options?: Options) => Promise<FunctionToMemoize>; | ||
invalidate: (id?: string) => Promise<any>; | ||
getCacheFilePath: (fnToMemoize: FunctionToMemoize, options: Options) => string; | ||
} | ||
declare function memoizeFs(options: MemoizeOptions): Memoizer; | ||
export = memoizeFs; | ||
``` | ||
## Options | ||
When memoizing a function all below options can be applied in any combination. | ||
The only required option is `cachePath`. | ||
#### cacheId | ||
### cachePath | ||
Path to the location of the cache on the disk. This option is always **required**. | ||
### cacheId | ||
By default all cache files are saved into the __root cache__ which is the folder specified by the cachePath option: | ||
```javascript | ||
var memoize = require('memoize-fs')({ cachePath: require('path').join(__dirname, '../../cache' }); | ||
```js | ||
var path = require('path') | ||
var memoizer = require('memoize-fs')({ cachePath: path.join(__dirname, '../../cache') }) | ||
``` | ||
@@ -119,7 +194,7 @@ | ||
```javascript | ||
memoize.fn(fun, { cacheId: 'foobar' }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { cacheId: 'foobar' }) | ||
``` | ||
#### salt | ||
### salt | ||
@@ -131,44 +206,74 @@ Functions may have references to variables outside their own scope. As a consequence two functions which look exactly the same | ||
```javascript | ||
memoize.fn(fun, { salt: 'foobar' }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { salt: 'foobar' }) | ||
``` | ||
#### maxAge | ||
### maxAge | ||
With `maxAge` option you can ensure that cache for given call is cleared after a predefined period of time (in milliseconds). | ||
```javascript | ||
memoize.fn(fun, { maxAge: 10000 }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { maxAge: 10000 }) | ||
``` | ||
#### force | ||
### force | ||
The `force` option forces the re-execution of an already memoized function and the re-caching of its outcome: | ||
```javascript | ||
memoize.fn(fun, { force: true }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { force: true }) | ||
``` | ||
#### astBody | ||
### astBody | ||
If you want to use the function AST instead the function body when generating the hash ([see serialization](#serialization)), set the option `astBody` to `true`. This allows the function source code to be reformatted without busting the cache. See https://github.com/borisdiakur/memoize-fs/issues/6 for details. | ||
```javascript | ||
memoize.fn(fun, { astBody: true }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { astBody: true }) | ||
``` | ||
#### noBody | ||
### noBody | ||
If for some reason you want to omit the function body when generating the hash ([see serialization](#serialization)), set the option `noBody` to `true`. | ||
```javascript | ||
memoize.fn(fun, { noBody: true }).then(... | ||
```js | ||
memoizer.fn(fnToMemoize, { noBody: true }) | ||
``` | ||
### Manual cache invalidation | ||
### serialize and deserialize | ||
These two options allows you to control how the serialization and deserialization process works. | ||
By default we use basic `JSON.stringify` and `JSON.parse`, but you may need more advanced stuff. | ||
In the following example we are using [Yahoo's `serialize-javascript`](https://ghub.now.sh/serialize-javascript) | ||
to be able to cache properly the return result of memoized function containing a `function`. | ||
```js | ||
const memoizeFs = require('memoize-fs') | ||
const serialize = require('serialize-javascript') | ||
const deserialize = (serializedJsString) => eval(`(${serializedJsString})`) | ||
const memoizer = memoizeFs({ cachePath: './cache', serialize, deserialize }) | ||
function someFn (a) { | ||
const bar = 123 | ||
setTimeout(() => {}, a * 10) | ||
return { | ||
bar, | ||
getBar() { return a + bar } | ||
} | ||
} | ||
memoizer.fn(someFn) | ||
``` | ||
## Manual cache invalidation | ||
You can delete the root cache (all cache files inside the folder specified by the cachePath option): | ||
```javascript | ||
memoize.invalidate().then(... | ||
```js | ||
memoizer.invalidate().then(() => { console.log('cache cleared') }) | ||
``` | ||
@@ -178,4 +283,4 @@ | ||
```javascript | ||
memoize.invalidate('foobar').then(... | ||
```js | ||
memoizer.invalidate('foobar').then(() => { console.log('cache for "foobar" cleared') }) | ||
``` | ||
@@ -185,2 +290,4 @@ | ||
See also the [`options.seriliaze` and `options.deserialize`](#serialize-and-deserialize). | ||
memoize-fs uses JSON to serialize the results of a memoized function. | ||
@@ -194,5 +301,5 @@ It also uses JSON, when it tries to serialize the arguments of the memoized function in order to create a hash | ||
```js | ||
var memoize = require('memoize-fs')({cachePath: '/'}) | ||
memoize.getCacheFilePath(function () {}, ['arg', 'arg'], {cacheId: 'foobar'}) | ||
// -> '/foobar/06f254...' | ||
var memoizer = require('memoize-fs')({ cachePath: './' }) | ||
memoizer.getCacheFilePath(function () {}, ['arg', 'arg'], { cacheId: 'foobar' }) | ||
// -> './foobar/06f254...' | ||
``` | ||
@@ -235,7 +342,9 @@ | ||
Lint with: | ||
```shell | ||
npm run jshint | ||
npm run lint | ||
``` | ||
Test with: | ||
```shell | ||
@@ -248,5 +357,5 @@ npm run mocha | ||
```shell | ||
npm run istanbul | ||
npm run cov | ||
``` | ||
Then please commit with a __detailed__ commit message. | ||
Then please commit with a **detailed** commit message. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
25054
2
275
350
11
6
+ Addedfs-extra@^8.1.0
+ Addedfs-extra@8.1.0(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedjsonfile@4.0.0(transitive)
+ Addeduniversalify@0.1.2(transitive)
- Removedmkdirp@^1.0.3
- Removedrimraf@^3.0.1
- Removedbalanced-match@1.0.2(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedfs.realpath@1.0.0(transitive)
- Removedglob@7.2.3(transitive)
- Removedinflight@1.0.6(transitive)
- Removedinherits@2.0.4(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedmkdirp@1.0.4(transitive)
- Removedonce@1.4.0(transitive)
- Removedpath-is-absolute@1.0.1(transitive)
- Removedrimraf@3.0.2(transitive)
- Removedwrappy@1.0.2(transitive)