diff --git a/README.md b/README.md index 6c84aec..15df115 100644 --- a/README.md +++ b/README.md @@ -52,10 +52,11 @@ module.exports = { | **`cacheKey`** | `{Function(options, request) -> {String}}` | `undefined` | Allows you to override default cache key generator | | **`cacheDirectory`** | `{String}` | `findCacheDir({ name: 'cache-loader' }) or os.tmpdir()` | Provide a cache directory where cache items should be stored (used for default read/write implementation) | | **`cacheIdentifier`** | `{String}` | `cache-loader:{version} {process.env.NODE_ENV}` | Provide an invalidation identifier which is used to generate the hashes. You can use it for extra dependencies of loaders (used for default read/write implementation) | -| **`write`** | `{Function(cacheKey, data, callback) -> {void}}` | `undefined` | Allows you to override default write cache data to file (e.g. Redis, memcached) | +| **`compare`** | `{Function(stats, dep) -> {Boolean}}` | `undefined` | Allows you to override default comparison function between the cached dependency and the one is being read. Return `true` to use the cached resource | +| **`precision`** | `{Number}` | `0` | Round `mtime` by this number of milliseconds both for `stats` and `dep` before passing those params to the comparing function | | **`read`** | `{Function(cacheKey, callback) -> {void}}` | `undefined` | Allows you to override default read cache data from file | -| **`compare`** | `{Function(stats, dep) -> {Boolean}}` | `undefined` | Allows you to override default comparison function between the cached dependency and the one is being read. Return `true` to use the cached resource. | | **`readOnly`** | `{Boolean}` | `false` | Allows you to override default value and make the cache read only (useful for some environments where you don't want the cache to be updated, only read from it) | +| **`write`** | `{Function(cacheKey, data, callback) -> {void}}` | `undefined` | Allows you to override default write cache data to file (e.g. Redis, memcached) | ## Examples diff --git a/src/index.js b/src/index.js index 54f4957..c4009ba 100644 --- a/src/index.js +++ b/src/index.js @@ -24,10 +24,11 @@ const defaults = { cacheDirectory: findCacheDir({ name: 'cache-loader' }) || os.tmpdir(), cacheIdentifier: `cache-loader:${pkg.version} ${env}`, cacheKey, + compare, + precision: 0, read, readOnly: false, write, - compare, }; function pathWithCacheContext(cacheContext, originalPath) { @@ -48,6 +49,10 @@ function pathWithCacheContext(cacheContext, originalPath) { .join('!'); } +function roundMs(mtime, precision) { + return Math.floor(mtime / precision) * precision; +} + function loader(...args) { const options = Object.assign({}, defaults, getOptions(this)); validateOptions(schema, options, 'Cache Loader'); @@ -135,11 +140,12 @@ function pitch(remainingRequest, prevRequest, dataInput) { validateOptions(schema, options, 'Cache Loader (Pitch)'); const { - read: readFn, - readOnly, cacheContext, cacheKey: cacheKeyFn, compare: compareFn, + read: readFn, + readOnly, + precision, } = options; const callback = this.async(); @@ -175,9 +181,23 @@ function pitch(remainingRequest, prevRequest, dataInput) { return; } + const compStats = stats; + const compDep = dep; + if (precision > 1) { + ['atime', 'mtime', 'ctime', 'birthtime'].forEach((key) => { + const msKey = `${key}Ms`; + const ms = roundMs(stats[msKey], precision); + + compStats[msKey] = ms; + compStats[key] = new Date(ms); + }); + + compDep.mtime = roundMs(dep.mtime, precision); + } + // If the compare function returns false // we not read from cache - if (compareFn(stats, dep) !== true) { + if (compareFn(compStats, compDep) !== true) { eachCallback(true); return; } diff --git a/src/options.json b/src/options.json index be497ae..0d6c2a4 100644 --- a/src/options.json +++ b/src/options.json @@ -13,6 +13,12 @@ "cacheDirectory": { "type": "string" }, + "compare": { + "instanceof": "Function" + }, + "precision": { + "type": "number" + }, "read": { "instanceof": "Function" }, @@ -21,9 +27,6 @@ }, "write": { "instanceof": "Function" - }, - "compare": { - "instanceof": "Function" } }, "additionalProperties": false diff --git a/test/compare-option.test.js b/test/compare-option.test.js index 1481324..426a632 100644 --- a/test/compare-option.test.js +++ b/test/compare-option.test.js @@ -1,11 +1,13 @@ +const fs = require('fs'); + const { webpack } = require('./helpers'); const mockCacheLoaderCompareFn = jest.fn(); const mockWebpackConfig = { loader: { options: { - compare: () => { - mockCacheLoaderCompareFn(); + compare: (stats, dep) => { + mockCacheLoaderCompareFn(stats, dep); return true; }, }, @@ -13,10 +15,39 @@ const mockWebpackConfig = { }; describe('compare option', () => { + beforeEach(() => { + mockCacheLoaderCompareFn.mockClear(); + }); + it('should call compare function', async () => { const testId = './basic/index.js'; await webpack(testId, mockWebpackConfig); await webpack(testId, mockWebpackConfig); expect(mockCacheLoaderCompareFn).toHaveBeenCalled(); }); + + it('should call compare function with 2 args', async () => { + const testId = './basic/index.js'; + await webpack(testId, mockWebpackConfig); + await webpack(testId, mockWebpackConfig); + expect(mockCacheLoaderCompareFn).toHaveBeenCalled(); + expect(mockCacheLoaderCompareFn.mock.calls[0].length).toBe(2); + }); + + it('should call compare function with correct args', async () => { + const testId = './basic/index.js'; + await webpack(testId, mockWebpackConfig); + await webpack(testId, mockWebpackConfig); + expect(mockCacheLoaderCompareFn).toHaveBeenCalled(); + + // eslint-disable-next-line + const stats = mockCacheLoaderCompareFn.mock.calls[0][0]; + // eslint-disable-next-line + const dep = mockCacheLoaderCompareFn.mock.calls[0][1]; + expect(stats).toBeDefined(); + expect(stats instanceof fs.Stats); + expect(dep).toBeDefined(); + expect(dep.mtime).toBeDefined(); + expect(dep.path).toBeDefined(); + }); }); diff --git a/test/precision-option.test.js b/test/precision-option.test.js new file mode 100644 index 0000000..6daecb2 --- /dev/null +++ b/test/precision-option.test.js @@ -0,0 +1,72 @@ +const { webpack } = require('./helpers'); + +const mockCacheLoaderCompareFn = jest.fn(); +const mockCacheLoaderCompareWithPrecisionFn = jest.fn(); +const mockWebpackConfig = { + loader: { + options: { + compare: (stats, dep) => { + mockCacheLoaderCompareFn(stats, dep); + return true; + }, + }, + }, +}; +const mockWebpackWithPrecisionConfig = { + loader: { + options: { + compare: (stats, dep) => { + mockCacheLoaderCompareWithPrecisionFn(stats, dep); + return true; + }, + precision: 1000, + }, + }, +}; + +describe('precision option', () => { + beforeEach(() => { + mockCacheLoaderCompareFn.mockClear(); + mockCacheLoaderCompareWithPrecisionFn.mockClear(); + }); + + it('should not apply precision', async () => { + const testId = './basic/index.js'; + await webpack(testId, mockWebpackConfig); + mockCacheLoaderCompareFn.mockClear(); + + await webpack(testId, mockWebpackConfig); + + const pastPrecisionTime = mockCacheLoaderCompareFn.mock.calls[0][1].mtime; + mockCacheLoaderCompareFn.mockClear(); + + await webpack(testId, mockWebpackConfig); + expect(pastPrecisionTime).toBe( + mockCacheLoaderCompareFn.mock.calls[0][1].mtime + ); + }); + + it('should call compare with values after applying precision', async () => { + const testId = './basic/index.js'; + await webpack(testId, mockWebpackConfig); + mockCacheLoaderCompareFn.mockClear(); + await webpack(testId, mockWebpackConfig); + await webpack(testId, mockWebpackWithPrecisionConfig); + expect(mockCacheLoaderCompareFn.mock.calls[0][1].mtime).not.toBe( + mockCacheLoaderCompareWithPrecisionFn.mock.calls[0][1].mtime + ); + }); + + it('should apply precision dividing by the value', async () => { + const testId = './basic/index.js'; + await webpack(testId, mockWebpackConfig); + mockCacheLoaderCompareFn.mockClear(); + await webpack(testId, mockWebpackConfig); + await webpack(testId, mockWebpackWithPrecisionConfig); + + const newMtime = + mockCacheLoaderCompareWithPrecisionFn.mock.calls[0][1].mtime; + const oldMtime = mockCacheLoaderCompareFn.mock.calls[0][1].mtime; + expect(newMtime).toBe(Math.floor(oldMtime / 1000) * 1000); + }); +});