Skip to content

Commit

Permalink
FEAT: Add an option to not prune non visited keys. Closes #2
Browse files Browse the repository at this point in the history
  • Loading branch information
royriojas committed Aug 1, 2016
1 parent 3ca70d8 commit b1a64db
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 60 deletions.
39 changes: 25 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# file-entry-cache
> Super simple cache for file metadata, useful for process that work o a given series of files
> Super simple cache for file metadata, useful for process that work o a given series of files
> and that only need to repeat the job on the changed ones since the previous run of the process — Edit
[![NPM Version](http://img.shields.io/npm/v/file-entry-cache.svg?style=flat)](https://npmjs.org/package/file-entry-cache)
Expand All @@ -14,7 +14,7 @@ npm i --save file-entry-cache
## Usage

```js
// loads the cache, if one does not exists for the given
// loads the cache, if one does not exists for the given
// Id a new one will be prepared to be created
var fileEntryCache = require('file-entry-cache');

Expand All @@ -25,18 +25,23 @@ var files = expand('../fixtures/*.txt');
// the first time this method is called, will return all the files
var oFiles = cache.getUpdatedFiles(files);

// this will persist this to disk checking each file stats and
// this will persist this to disk checking each file stats and
// updating the meta attributes `size` and `mtime`.
// custom fields could also be added to the meta object and will be persisted
// in order to retrieve them later
cache.reconcile();
cache.reconcile();

// use this if you want the non visited file entries to be kept in the cache
// for more than one execution
//
// cache.reconcile( true /* noPrune */)

// on a second run
var cache2 = fileEntryCache.create('testCache');

// will return now only the files that were modified or none
// if no files were modified previous to the execution of this function
var oFiles = cache.getUpdatedFiles(files);
var oFiles = cache.getUpdatedFiles(files);

// if you want to prevent a file from being considered non modified
// something useful if a file failed some sort of validation
Expand Down Expand Up @@ -64,32 +69,38 @@ entry = {

## Motivation for this module

I needed a super simple and dumb **in-memory cache** with optional disk persistence (write-back cache) in order to make
I needed a super simple and dumb **in-memory cache** with optional disk persistence (write-back cache) in order to make
a script that will beautify files with `esformatter` to execute only on the files that were changed since the last run.

In doing so the process of beautifying files was reduced from several seconds to a small fraction of a second.

This module uses [flat-cache](https://www.npmjs.com/package/flat-cache) a super simple `key/value` cache storage with
This module uses [flat-cache](https://www.npmjs.com/package/flat-cache) a super simple `key/value` cache storage with
optional file persistance.

The main idea is to read the files when the task begins, apply the transforms required, and if the process succeed,
then store the new state of the files. The next time this module request for `getChangedFiles` will return only
The main idea is to read the files when the task begins, apply the transforms required, and if the process succeed,
then store the new state of the files. The next time this module request for `getChangedFiles` will return only
the files that were modified. Making the process to end faster.

This module could also be used by processes that modify the files applying a transform, in that case the result of the
transform could be stored in the `meta` field, of the entries. Anything added to the meta field will be persisted.
Those processes won't need to call `getChangedFiles` they will instead call `normalizeEntries` that will return the
transform could be stored in the `meta` field, of the entries. Anything added to the meta field will be persisted.
Those processes won't need to call `getChangedFiles` they will instead call `normalizeEntries` that will return the
entries with a `changed` field that can be used to determine if the file was changed or not. If it was not changed
the transformed stored data could be used instead of actually applying the transformation, saving time in case of only
a few files changed.

In the worst case scenario all the files will be processed. In the best case scenario only a few of them will be processed.

## Important notes
- The values set on the meta attribute of the entries should be `stringify-able` ones, meaning no circular references
- All the changes to the cache state are done to memory first and only persisted after reconcile
- The values set on the meta attribute of the entries should be `stringify-able` ones if possible, flat-cache uses `circular-json` to try to persist circular structures, but this should be considered experimental. The best results are always obtained with non circular values
- All the changes to the cache state are done to memory first and only persisted after reconcile.
- By default non visited entries are removed from the cache. This is done to prevent the file from growing too much. If this is not an issue and
you prefer to do a manual pruning of the cache files, you can pass `true` to the `reconcile` call. Like this:

```javascript
cache.reconcile( true /* noPrune */ );
```

## License
## License

MIT

Expand Down
10 changes: 6 additions & 4 deletions cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {

return {
/**
* the flat cache storage used to persist the metadata of the files
* the flat cache storage used to persist the metadata of the `files
* @type {Object}
*/
cache: cache,
Expand Down Expand Up @@ -161,16 +161,18 @@ module.exports = {
},
/**
* Sync the files and persist them to the cache
*
* @param [noPrune=false] {Boolean} whether to remove non visited/saved entries
* @method reconcile
*/
reconcile: function () {
reconcile: function ( noPrune ) {
var entries = normalizedEntries;

var keys = Object.keys( entries );

if ( keys.length === 0 ) {
return;
}

keys.forEach( function ( entryName ) {
var cacheEntry = entries[ entryName ];
var stat = fs.statSync( cacheEntry.key );
Expand All @@ -183,7 +185,7 @@ module.exports = {
cache.setKey( entryName, meta );
} );

cache.save();
cache.save( noPrune );
}
};
}
Expand Down
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
"node": ">=0.10.0"
},
"scripts": {
"beautify": "esbeautifier 'cache.js' 'specs/**/*.js'",
"beautify-check": "esbeautifier -k 'cache.js' 'specs/**/*.js'",
"beautify": "esbeautifier 'cache.js' 'test/**/*.js'",
"beautify-check": "npm run beautify -- -k",
"eslint": "eslinter 'cache.js' 'specs/**/*.js'",
"lint": "npm run beautify && npm run eslint",
"verify": "npm run beautify-check && npm run eslint",
"install-hooks": "prepush install && changelogx install-hook && precommit install",
"changelog": "changelogx -f markdown -o ./changelog.md",
"do-changelog": "npm run changelog && git add ./changelog.md && git commit -m 'DOC: Generate changelog' --no-verify",
"pre-v": "npm run verify",
"pre-v": "npm run test",
"post-v": "npm run do-changelog && git push --no-verify && git push --tags --no-verify",
"bump-major": "npm run pre-v && npm version major -m 'BLD: Release v%s' && npm run post-v",
"bump-minor": "npm run pre-v && npm version minor -m 'BLD: Release v%s' && npm run post-v",
"bump-patch": "npm run pre-v && npm version patch -m 'BLD: Release v%s' && npm run post-v",
"test": "mocha -R spec test/specs",
"test": "npm run verify && mocha -R spec test/specs",
"cover": "istanbul cover test/runner.js html text-summary",
"watch": "watch-run -i -p 'test/specs/**/*.js' istanbul cover test/runner.js html text-summary"
},
Expand Down Expand Up @@ -72,14 +72,13 @@
"precommit": "^1.1.5",
"prepush": "^3.1.4",
"proxyquire": "^1.3.1",
"read-file": "^0.2.0",
"sinon": "^1.12.2",
"sinon-chai": "^2.7.0",
"watch-run": "^1.2.1",
"write": "^0.2.1"
"write": "^0.3.1"
},
"dependencies": {
"flat-cache": "^1.0.9",
"flat-cache": "^1.2.1",
"object-assign": "^4.0.1"
}
}
34 changes: 15 additions & 19 deletions test/runner.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,40 @@
"use strict";

// we run mocha manually otherwise istanbul coverage won't work
// run `npm test --coverage` to generate coverage report

var Mocha = require('mocha');


// ---

var Mocha = require( 'mocha' );


// to set these options run the test script like:
// > BAIL=true GREP=array_expression REPORTER=dot npm test
var opts = {
ui: 'bdd',
bail: !!(process.env.BAIL),
reporter:( process.env.REPORTER || 'spec'),
reporter: (process.env.REPORTER || 'spec'),
grep: process.env.GREP,
colors: true
};

// we use the dot reporter on travis since it works better
if (process.env.TRAVIS) {
if ( process.env.TRAVIS ) {
opts.reporter = 'dot';
}

var m = new Mocha(opts);
var m = new Mocha( opts );

if (process.env.INVERT) {
if ( process.env.INVERT ) {
m.invert();
}

var expand = require('glob-expand');
expand('test/specs/**/*.js').forEach(function (file){
m.addFile(file);
});
var expand = require( 'glob-expand' );
expand( 'test/specs/**/*.js' ).forEach( function ( file ) {
m.addFile( file );
} );

m.run(function(err) {
m.run( function ( err ) {
var exitCode = err ? 1 : 0;
if (err) console.log('failed tests: ' + err);
process.exit(exitCode);
});
if ( err ) {
console.log( 'failed tests: ' + err );
}

process.exit( exitCode ); // eslint-disable-line
} );
81 changes: 65 additions & 16 deletions test/specs/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ describe( 'file-entry-cache', function () {
var expect = require( 'chai' ).expect;
var path = require( 'path' );
var write = require( 'write' ).sync;
var read = require( 'read-file' ).readFileSync;
var fileEntryCache = require( '../../cache' );

function expand() {
return require( 'glob-expand' ).apply(null, arguments).map(function (file) {
return file.split('/').join(path.sep);
});
};
return require( 'glob-expand' ).apply( null, arguments ).map( function ( file ) {
return file.split( '/' ).join( path.sep );
} );
}

var fixturesDir = path.resolve( __dirname, '../fixtures' );
var del = require( 'del' ).sync;
Expand All @@ -37,9 +36,7 @@ describe( 'file-entry-cache', function () {
var cache;

var delCacheAndFiles = function () {
del( fixturesDir, {
force: true
} );
del( fixturesDir, { force: true } );
cache && cache.deleteCacheFile();
};

Expand Down Expand Up @@ -80,8 +77,6 @@ describe( 'file-entry-cache', function () {

cache.reconcile();

//write( file, 'some different content');

expect( cache.hasFileChanged( file ) ).to.be.false;

} );
Expand Down Expand Up @@ -128,7 +123,7 @@ describe( 'file-entry-cache', function () {
cache.reconcile();

oFiles = cache.getUpdatedFiles( files );
expect( oFiles ).to.deep.equal( [] );
expect( oFiles ).to.deep.equal( [ ] );

cache.deleteCacheFile();

Expand Down Expand Up @@ -191,7 +186,7 @@ describe( 'file-entry-cache', function () {
cache = fileEntryCache.create( 'testCache2' );
var files = cache.getUpdatedFiles( null );
cache.reconcile();
expect( files ).to.deep.equal( [] );
expect( files ).to.deep.equal( [ ] );
} );

it( 'should not fail if calling reconcile without a prior call to `getChangedFiles` or `normalizeEntries`', function () {
Expand All @@ -216,6 +211,62 @@ describe( 'file-entry-cache', function () {

} );

it( 'should remove non visited entries from the cache', function () {
cache = fileEntryCache.create( 'testCache' );

var files = expand( path.resolve( __dirname, '../fixtures/*.txt' ) );
cache.normalizeEntries( files );

cache.reconcile();

// the f2.txt file is in the cache
expect( cache.cache.getKey( path.resolve( __dirname, '../fixtures/f2.txt' ) ) ).to.not.equal( undefined );

// load the cache again
cache = fileEntryCache.create( 'testCache' );

// when recently loaded all entries are in the cache
expect( cache.cache.keys().length ).to.equal( 4 );

// we check only one file
expect( cache.hasFileChanged( path.resolve( __dirname, '../fixtures/f4.txt' ) ) ).to.be.false;

cache.reconcile();

// after reconcile we will only have 1 key because we just visited one entry
expect( cache.cache.getKey( path.resolve( __dirname, '../fixtures/f2.txt' ) ) ).to.equal( undefined );
expect( cache.cache.keys().length ).to.equal( 1 );

} );

it( 'should not remove visited entries from the cache if reconcile is called with noPrune=true', function () {
cache = fileEntryCache.create( 'testCache' );

var files = expand( path.resolve( __dirname, '../fixtures/*.txt' ) );
cache.normalizeEntries( files );

cache.reconcile();

// the f2.txt file is in the cache
expect( cache.cache.getKey( path.resolve( __dirname, '../fixtures/f2.txt' ) ) ).to.not.equal( undefined );

// load the cache again
cache = fileEntryCache.create( 'testCache' );

// when recently loaded all entries are in the cache
expect( cache.cache.keys().length ).to.equal( 4 );

// we check only one file
expect( cache.hasFileChanged( path.resolve( __dirname, '../fixtures/f4.txt' ) ) ).to.be.false;

cache.reconcile( true );

// after reconcile we will only have 1 key because we just visited one entry
expect( cache.cache.getKey( path.resolve( __dirname, '../fixtures/f2.txt' ) ) ).to.not.equal( undefined );
expect( cache.cache.keys().length ).to.equal( 4 );

} );

it( 'should return fileDescriptor for all the passed files', function () {
cache = fileEntryCache.create( 'testCache' );

Expand All @@ -241,7 +292,7 @@ describe( 'file-entry-cache', function () {
it( 'should not fail if a null array of files is passed', function () {
cache = fileEntryCache.create( 'testCache' );
var oFiles = cache.normalizeEntries( null );
expect( oFiles ).to.deep.equal( [] );
expect( oFiles ).to.deep.equal( [ ] );
} );

} );
Expand All @@ -258,9 +309,7 @@ describe( 'file-entry-cache', function () {
var files = expand( path.resolve( __dirname, '../fixtures/*.txt' ) );
var entries = cache.normalizeEntries( files );

entries[ 1 ].meta.data = {
foo: 'bar'
};
entries[ 1 ].meta.data = { foo: 'bar' };
entries[ 2 ].meta.data = {
baz: {
some: 'foo'
Expand Down

0 comments on commit b1a64db

Please sign in to comment.