From 0340229d24aabba3cbdafd33c07ef7518510595a Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 00:45:46 +0200 Subject: [PATCH 1/7] First commit for cache --- src/Cache.js | 87 ++++++++++++++++++++++++++++++++++++ src/Parser.js | 16 ++++--- test/unit/CacheTest.js | 92 +++++++++++++++++++++++++++++++++++++++ test/unit/CompleteTest.js | 1 + 4 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 src/Cache.js create mode 100644 test/unit/CacheTest.js diff --git a/src/Cache.js b/src/Cache.js new file mode 100644 index 0000000..9948904 --- /dev/null +++ b/src/Cache.js @@ -0,0 +1,87 @@ +const Analyser = require('./Analyser'); +const Crypto = require('crypto'); +const simpleCache = new Map(); +let cacheCleanerInterval; + +/** + * Class that enable caching of results + */ +class Cache { + /** + * Analyse the provided headers or User-Agent string + * + * @param {object|string} headers An object with all of the headers or a string with just the User-Agent header + * @param {object} options An object with configuration options + * @param {object} that An object that is the Parser this + * + * @return {boolean|object} Returns true if the cache is active but no match found, return the match if found + */ + static analyseWithCache(headers, options, that) { + if (options.cache) { + const Parser = require('./Parser'); + options.cacheExpires = options.cacheExpires || 900; + options.cacheCheckInterval = options.cacheCheckInterval || parseInt(options.cacheExpires / 5, 10); + switch (options.cache) { + case Parser.SIMPLE_CACHE: { + if (!cacheCleanerInterval && options.cacheExpires) { + cacheCleanerInterval = setInterval(Cache.clearCache, options.cacheCheckInterval * 1000); + } + const chiper = Crypto.createHash('sha256').update(JSON.stringify(headers)).digest('hex'); + if (simpleCache.has(chiper)) { + const hit = simpleCache.get(chiper); + hit.expires = Cache.getExpirationValue(options); + return hit.data; + } else { + const analyser = new Analyser(headers, options); + analyser.setData(that); + analyser.analyse(); + simpleCache.set(chiper, { + data: { + browser: that.browser, + engine: that.engine, + os: that.os, + device: that.device, + camouflage: that.camouflage, + features: that.features, + }, + expires: Cache.getExpirationValue(options), + }); + } + } + } + return true; + } + return false; + } + + /** + * Check if data in cache is expired + */ + static clearCache() { + simpleCache.forEach((v, k) => { + if (v.expires < +new Date()) { + simpleCache.delete(k); + } + }); + } + + /** + * Compute the expiration time + * + * @param {object} options An object with configuration options + * + * @return {int} The expiration time + */ + static getExpirationValue(options) { + return +new Date() + options.cacheExpires * 1000; + } + + /** + * Clear the cached. Used only for test purpose + */ + static purgeCache() { + simpleCache.clear(); + } +} + +module.exports = Cache; diff --git a/src/Parser.js b/src/Parser.js index cc85dbe..4ec7051 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -1,5 +1,7 @@ const Main = require('./model/Main'); const Analyser = require('./Analyser'); +const Cache = require('./Cache'); + /** * Class that parse the user-agent */ @@ -37,15 +39,19 @@ class Parser extends Main { } else { h = headers; } - - /* if (this.analyseWithCache(h, o)) { - return; - }*/ + let data; + if ((data = Cache.analyseWithCache(h, o, this))) { + if (typeof data === 'object') { + Object.assign(this, data); + this.cached = true; + } + return; + } const analyser = new Analyser(h, o); analyser.setData(this); analyser.analyse(); } } - +Parser.SIMPLE_CACHE = 'simple'; module.exports = Parser; diff --git a/test/unit/CacheTest.js b/test/unit/CacheTest.js new file mode 100644 index 0000000..d148e9a --- /dev/null +++ b/test/unit/CacheTest.js @@ -0,0 +1,92 @@ +const {describe, it, beforeEach, before, after} = (exports.lab = require('lab').script()); +const expect = require('code').expect; +const Sinon = require('sinon'); +const Parser = require('../../src/Parser'); +const Cache = require('../../src/Cache'); +const header1 = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1)'; +const header2 = + 'User-Agent: Mozilla/5.0 (Linux; Android 4.0.3; GT-P3113 Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Safari/535.19'; +const result1 = { + browser: {name: 'Internet Explorer', version: '6.0', type: 'browser'}, + engine: {name: 'Trident'}, + os: {name: 'Windows', version: {value: '5.0', alias: '2000'}}, + device: {type: 'desktop'}, +}; +const result2 = { + browser: {name: 'Chrome', version: '18', type: 'browser'}, + engine: {name: 'Webkit', version: '535.19'}, + os: {name: 'Android', version: '4.0.3'}, + device: {type: 'tablet', manufacturer: 'Samsung', model: 'Galaxy Tab 2 7.0'}, +}; +let clock; + +describe('Cache Class', () => { + beforeEach((done) => { + Cache.purgeCache(); + done(); + }); + describe('Two subsequent parse with same UA', () => { + it('should be cached', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.not.exist(); + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.be.true(); + + done(); + }); + }); + + describe('Two subsequent parse with different UA', () => { + it('should not be cached', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.not.exist(); + result = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(result2); + expect(result.cached).to.not.exist(); + done(); + }); + }); + + before((done) => { + clock = Sinon.useFakeTimers(); + done(); + }); + after((done) => { + clock.restore(); + done(); + }); + describe('Default cache with default expiry time and expiry check timer', () => { + it('should be empty after expiry time 900s + 1/5 * 900s + 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.not.exist(); + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.be.true(); + clock.tick((900 + 10 + 900 / 5) * 1000); + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(result1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Creating Parse without arguments', () => { + it('an calling analyse should work', (done) => { + const parser = new Parser(); + parser.analyse('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1)'); + + expect(parser).to.be.instanceOf(Parser); + + expect(parser.isBrowser('Internet Explorer', '=', '6.0')).to.be.true(); + done(); + }); + }); +}); diff --git a/test/unit/CompleteTest.js b/test/unit/CompleteTest.js index dd48a59..7556eaa 100644 --- a/test/unit/CompleteTest.js +++ b/test/unit/CompleteTest.js @@ -29,6 +29,7 @@ function makeTest(options) { expect(parserObj.toString()).to.be.equal(options.readable); expect(parserObj.toObject()).to.be.equal(options.result); + expect(parserObj.cached).to.not.exists(); done(); }); } From a8f396264a52b17ce59f8df2a91311d0c1721c5b Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 12:55:30 +0200 Subject: [PATCH 2/7] Renamed Test file --- test/unit/{CompleteTest.js => MassiveTest.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/unit/{CompleteTest.js => MassiveTest.js} (100%) diff --git a/test/unit/CompleteTest.js b/test/unit/MassiveTest.js similarity index 100% rename from test/unit/CompleteTest.js rename to test/unit/MassiveTest.js From f2c9b30103c285fe4a091c0f3ed6a3be17d25a91 Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 16:13:29 +0200 Subject: [PATCH 3/7] Cache Tests --- src/Cache.js | 41 +++-- test/unit/CacheTest.js | 386 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 390 insertions(+), 37 deletions(-) diff --git a/src/Cache.js b/src/Cache.js index 9948904..a84c95e 100644 --- a/src/Cache.js +++ b/src/Cache.js @@ -2,6 +2,7 @@ const Analyser = require('./Analyser'); const Crypto = require('crypto'); const simpleCache = new Map(); let cacheCleanerInterval; +let actualCacheCheckInterval; /** * Class that enable caching of results @@ -19,23 +20,34 @@ class Cache { static analyseWithCache(headers, options, that) { if (options.cache) { const Parser = require('./Parser'); - options.cacheExpires = options.cacheExpires || 900; - options.cacheCheckInterval = options.cacheCheckInterval || parseInt(options.cacheExpires / 5, 10); + options.cacheExpires = options.cacheExpires <= 0 ? 0 : options.cacheExpires || 900; + options.cacheCheckInterval = Math.max(options.cacheCheckInterval || parseInt(options.cacheExpires / 5, 10), 1); switch (options.cache) { case Parser.SIMPLE_CACHE: { - if (!cacheCleanerInterval && options.cacheExpires) { + /* Should set the clearCache interval only if cacheExpires is > 0 and it hasn't yet initializated + or the cacheCheckInterval has changed */ + if ( + (!cacheCleanerInterval || actualCacheCheckInterval !== options.cacheCheckInterval) && + options.cacheExpires + ) { + clearInterval(cacheCleanerInterval); cacheCleanerInterval = setInterval(Cache.clearCache, options.cacheCheckInterval * 1000); } - const chiper = Crypto.createHash('sha256').update(JSON.stringify(headers)).digest('hex'); - if (simpleCache.has(chiper)) { - const hit = simpleCache.get(chiper); - hit.expires = Cache.getExpirationValue(options); + // Disable cache expiry loop if it set to 0s + if (!options.cacheExpires) { + clearInterval(cacheCleanerInterval); + } + actualCacheCheckInterval = options.cacheCheckInterval; + const cacheKey = Crypto.createHash('sha256').update(JSON.stringify(headers)).digest('hex'); + if (simpleCache.has(cacheKey)) { + const hit = simpleCache.get(cacheKey); + hit.expires = Cache.getExpirationTime(options); return hit.data; } else { const analyser = new Analyser(headers, options); analyser.setData(that); analyser.analyse(); - simpleCache.set(chiper, { + simpleCache.set(cacheKey, { data: { browser: that.browser, engine: that.engine, @@ -44,7 +56,7 @@ class Cache { camouflage: that.camouflage, features: that.features, }, - expires: Cache.getExpirationValue(options), + expires: Cache.getExpirationTime(options), }); } } @@ -70,17 +82,20 @@ class Cache { * * @param {object} options An object with configuration options * - * @return {int} The expiration time + * @return {int} The expiration time timestamp */ - static getExpirationValue(options) { + static getExpirationTime(options) { return +new Date() + options.cacheExpires * 1000; } /** - * Clear the cached. Used only for test purpose + * Reset the class state. Used only for test purpose */ - static purgeCache() { + static resetClassState() { simpleCache.clear(); + clearInterval(cacheCleanerInterval); + cacheCleanerInterval = null; + actualCacheCheckInterval = null; } } diff --git a/test/unit/CacheTest.js b/test/unit/CacheTest.js index d148e9a..698ac88 100644 --- a/test/unit/CacheTest.js +++ b/test/unit/CacheTest.js @@ -1,4 +1,4 @@ -const {describe, it, beforeEach, before, after} = (exports.lab = require('lab').script()); +const {describe, it, beforeEach, afterEach} = (exports.lab = require('lab').script()); const expect = require('code').expect; const Sinon = require('sinon'); const Parser = require('../../src/Parser'); @@ -6,13 +6,13 @@ const Cache = require('../../src/Cache'); const header1 = 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1)'; const header2 = 'User-Agent: Mozilla/5.0 (Linux; Android 4.0.3; GT-P3113 Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Safari/535.19'; -const result1 = { +const comparision1 = { browser: {name: 'Internet Explorer', version: '6.0', type: 'browser'}, engine: {name: 'Trident'}, os: {name: 'Windows', version: {value: '5.0', alias: '2000'}}, device: {type: 'desktop'}, }; -const result2 = { +const comparision2 = { browser: {name: 'Chrome', version: '18', type: 'browser'}, engine: {name: 'Webkit', version: '535.19'}, os: {name: 'Android', version: '4.0.3'}, @@ -22,17 +22,22 @@ let clock; describe('Cache Class', () => { beforeEach((done) => { - Cache.purgeCache(); + Cache.resetClassState(); + clock = Sinon.useFakeTimers('setInterval', 'clearInterval', 'Date'); + done(); + }); + afterEach((done) => { + clock.restore(); done(); }); describe('Two subsequent parse with same UA', () => { it('should be cached', (done) => { let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.not.exist(); result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.be.true(); done(); @@ -43,49 +48,382 @@ describe('Cache Class', () => { it('should not be cached', (done) => { let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.not.exist(); result = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result2); + expect(result.toObject()).to.be.equal(comparision2); expect(result.cached).to.not.exist(); done(); }); }); - before((done) => { - clock = Sinon.useFakeTimers(); - done(); + describe('Create cache with negative expiry time', () => { + it('should let the Cache never expire', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: -1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + // let pass 2 years + clock.tick(60 * 60 * 24 * 265 * 2 * 1000); + + // it's still in cache + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + done(); + }); }); - after((done) => { - clock.restore(); - done(); + + describe('Create cache with an expiry time that would make the cache check interval be 0s (cacheExpires/5), ', () => { + it('should instead be 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 4}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + // let pass 3 seconds + clock.tick(3 * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 4}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + // let pass 4 seconds + cache check inteval + clock.tick((4 + 1 + 1) * 1000); + + // No more in cache + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 4}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); }); + describe('Default cache with default expiry time and expiry check timer', () => { it('should be empty after expiry time 900s + 1/5 * 900s + 1s', (done) => { let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + clock.tick((900 + 900 / 5 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Default cache with default expiry time but with 1s expiry check timer', () => { + it('should be empty after expiry time 900s + 1s + 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + clock.tick((900 + 1 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Default cache with 100s expiry time and default expiry check timer', () => { + it('should be empty after expiry time 100s + 1/5 * 100s + 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + clock.tick((100 + 100 / 5 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Default cache with 100s expiry time but with 2s expiry check timer', () => { + it('should be empty after expiry time 100s + 2s + 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + clock.tick((100 + 2 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Default cache with default expiry time but with 1s expiry check timer, one item with cache refreshed and one not', () => { + it('the one refreshed should be in cache after the first expiry time (900 + 1 + 1), the other not', (done) => { + // First call to Parser, cache both + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + let result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + + // Second call, check that are cached + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.be.true(); + + // Make the timer be 1 second before the cache expiry time + clock.tick(899 * 1000); + + // Refresh only the record related to header 1 + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + // Make the cache expiry and wait that the check interval has passed + clock.tick(3 * 1000); + + // Call the parser with header1, it should be still in cache + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + // Call the parser with header2, it should NOT be in cache + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + done(); + }); + }); + + describe('Change the expiry time while cache is populated', () => { + it('should affect only new items, and updated the expiry time after a new cache hit', (done) => { + // First call to Parser, still not cached, expiry time default to 900s + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + + expect(result.cached).to.not.exist(); + + // let pass 200s + clock.tick(200 * 1000); + + // Put in cache a new item with 100s expiry time + let result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + + // let expire the second item + clock.tick((100 + 100 / 5 + 1) * 1000); + + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + + // First item is still in cache, parse it and update the expiry time + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + // Let expiry the first item. + clock.tick((100 + 100 / 5 + 1) * 1000); + + // Item has expired also if the first 900s are not passed + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + done(); + }); + }); + + describe('Parse UA with no expiry time', () => { + it('should never expire', (done) => { + // First call to Parser, still not cached, expiry time default to 900s + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 0}); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.not.exist(); + + // let pass 2 years + clock.tick(60 * 60 * 24 * 265 * 2 * 1000); + + // it's still in cache result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.be.true(); - clock.tick((900 + 10 + 900 / 5) * 1000); + + done(); + }); + }); + + describe('Disable cache expiry when cache has already some item inside,', () => { + it('also old item with expiry time should never expire', (done) => { + // First call to Parser, still not cached, expiry time default to 900s + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + // let pass half expiration time + clock.tick(450 * 1000); + + let result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheExpires: 0}); + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + + // let pass 2 years + clock.tick(60 * 60 * 24 * 265 * 2 * 1000); + + // it's still in cache result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); - expect(result.toObject()).to.be.equal(result1); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.be.true(); + + done(); + }); + }); + + describe('Enable cache expiry when cache has already some item inside with no expiry time,', () => { + it('old item are cleared at the first cache check interval ', (done) => { + // First call to Parser, still not cached, expiry time default to 900s + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 0}); + expect(result.toObject()).to.be.equal(comparision1); expect(result.cached).to.not.exist(); + // let pass 2 years + clock.tick(60 * 60 * 24 * 265 * 2 * 1000); + + // it's still in cache + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 0}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + let result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.not.exist(); + + // let 1/5 of the expiry time so the cache check is triggered + clock.tick((900 / 5 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.be.true(); + done(); }); }); - describe('Creating Parse without arguments', () => { - it('an calling analyse should work', (done) => { - const parser = new Parser(); - parser.analyse('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; InfoPath.1)'); + describe('Default cache with 100s expiry time but with 2s expiry check timer', () => { + it('should be empty after expiry time 100s + 2s + 1s', (done) => { + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + clock.tick((100 + 2 + 1) * 1000); + + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 100, cacheCheckInterval: 2}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); - expect(parser).to.be.instanceOf(Parser); + done(); + }); + }); + + describe('Default cache with default expiry time and default check interval, after a second parse that change the check internval', () => { + it('should correctly update the cache check loop', (done) => { + // First call to Parser, cache both + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + // Make the timer be 1 second before the cache expiry time + clock.tick(899 * 1000); + + let result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE, cacheCheckInterval: 1}); + + // Let the cache expire for header 1 and let the cache check interval pass + clock.tick((1 + 1 + 1) * 1000); + + // header 1 record should be gone, header 2 one shoul be there in cache + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + expect(result2.toObject()).to.be.equal(comparision2); + expect(result2.cached).to.be.true(); + + done(); + }); + }); + + describe('Default cache with default expiry time and default check interval: second parse is done without cache', () => { + it('the result returned should not come from cache', (done) => { + // First call to Parser, cache both + let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); + + // let pass some time + clock.tick(100 * 1000); + + // cache is populated with header1 record + result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.be.true(); + + // parse the same header without cache property in options + result = new Parser(header1, {cacheCheckInterval: 1}); + expect(result.toObject()).to.be.equal(comparision1); + expect(result.cached).to.not.exist(); - expect(parser.isBrowser('Internet Explorer', '=', '6.0')).to.be.true(); done(); }); }); From 0ba19a05568fd8e06a13ccacbcb6db0d03b89bf1 Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 16:22:16 +0200 Subject: [PATCH 4/7] Bump up package version. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c2e9afa..a3c75c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "which-browser", - "version": "0.1.0", - "description": "Browser sniffing tool and UA parser. This library is a porting of WhichBrowser/Parser", + "version": "0.2.0", + "description": "Browser sniffing tool and UA parser. Porting to VanillaJS of the PHP library WhichBrowser/Parser", "main": "src/Parser.js", "scripts": { "test": "lab -a code -v", From 3b724cbc48451b120ac8bc08c65e34ad685760da Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 17:52:30 +0200 Subject: [PATCH 5/7] Updated ReadMe --- README.md | 62 ++++++++++++++++++++++++------------------ src/Cache.js | 10 +++---- src/Parser.js | 4 ++- test/unit/CacheTest.js | 10 +++---- 4 files changed, 48 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index ba67097..061a366 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ result.os.toString(); Or access parts of these properties directly: -```php +```js result.browser.name; // Chrome @@ -172,7 +172,7 @@ result.engine.name; Finally you can also query versions directly: -```php +```js result.browser.version.is('>', 26); // true @@ -192,46 +192,56 @@ In some cases you may want to disable the detection of bots. This allows the bot ```js result = new WhichBrowser(request.headers, { detectBots: false }); ``` - +> Be aware that changing the `cacheExpiries` *doesn't* impact the expiry date of others records in the cache. However it *will change* the rate at which the cache validity is checked as it does `cacheCheckInterval`. For example setting `cacheExpiries` to `0` will prevent **ALL** results to expire because it will disable the cache validity check. API reference ------------- diff --git a/src/Cache.js b/src/Cache.js index a84c95e..817e922 100644 --- a/src/Cache.js +++ b/src/Cache.js @@ -20,7 +20,7 @@ class Cache { static analyseWithCache(headers, options, that) { if (options.cache) { const Parser = require('./Parser'); - options.cacheExpires = options.cacheExpires <= 0 ? 0 : options.cacheExpires || 900; + options.cacheExpires = options.cacheExpires <= 0 ? Number.MAX_SAFE_INTEGER : options.cacheExpires || 900; options.cacheCheckInterval = Math.max(options.cacheCheckInterval || parseInt(options.cacheExpires / 5, 10), 1); switch (options.cache) { case Parser.SIMPLE_CACHE: { @@ -33,10 +33,6 @@ class Cache { clearInterval(cacheCleanerInterval); cacheCleanerInterval = setInterval(Cache.clearCache, options.cacheCheckInterval * 1000); } - // Disable cache expiry loop if it set to 0s - if (!options.cacheExpires) { - clearInterval(cacheCleanerInterval); - } actualCacheCheckInterval = options.cacheCheckInterval; const cacheKey = Crypto.createHash('sha256').update(JSON.stringify(headers)).digest('hex'); if (simpleCache.has(cacheKey)) { @@ -85,7 +81,9 @@ class Cache { * @return {int} The expiration time timestamp */ static getExpirationTime(options) { - return +new Date() + options.cacheExpires * 1000; + return options.cacheExpires === Number.MAX_SAFE_INTEGER + ? Number.MAX_SAFE_INTEGER + : +new Date() + options.cacheExpires * 1000; } /** diff --git a/src/Parser.js b/src/Parser.js index 4ec7051..76458cc 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -10,7 +10,9 @@ class Parser extends Main { * Create a new object that contains all the detected information * * @param {object|string} headers Optional, an object with all of the headers or a string with just the User-Agent header - * @param {object} options Optional, an object with configuration options + * @param {object} options Optional, an object with configuration options + * @param {int} [options.cacheExpires=900] Expiry time in seconds + * @param {int} [options.cacheCheckInterval=1/5 * options.cacheExpires] Time in seconds between each cache check to remove expired records. Minimum 1 */ constructor(headers = null, options = {}) { super(); diff --git a/test/unit/CacheTest.js b/test/unit/CacheTest.js index 698ac88..fd45ed8 100644 --- a/test/unit/CacheTest.js +++ b/test/unit/CacheTest.js @@ -322,7 +322,7 @@ describe('Cache Class', () => { }); describe('Enable cache expiry when cache has already some item inside with no expiry time,', () => { - it('old item are cleared at the first cache check interval ', (done) => { + it('Old items are still there when the new expiry time come', (done) => { // First call to Parser, still not cached, expiry time default to 900s let result = new Parser(header1, {cache: Parser.SIMPLE_CACHE, cacheExpires: 0}); expect(result.toObject()).to.be.equal(comparision1); @@ -340,16 +340,16 @@ describe('Cache Class', () => { expect(result2.toObject()).to.be.equal(comparision2); expect(result2.cached).to.not.exist(); - // let 1/5 of the expiry time so the cache check is triggered - clock.tick((900 / 5 + 1) * 1000); + // expiry the second record + clock.tick((900 + 900 / 5 + 1) * 1000); result = new Parser(header1, {cache: Parser.SIMPLE_CACHE}); expect(result.toObject()).to.be.equal(comparision1); - expect(result.cached).to.not.exist(); + expect(result.cached).to.be.true(); result2 = new Parser(header2, {cache: Parser.SIMPLE_CACHE}); expect(result2.toObject()).to.be.equal(comparision2); - expect(result2.cached).to.be.true(); + expect(result2.cached).to.not.exists(); done(); }); From 6d9f81048c13f05ea6d2789e2e52d98f72f0f3cd Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 17:56:46 +0200 Subject: [PATCH 6/7] Update to remove .vscode from dist package --- .npmignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.npmignore b/.npmignore index 099f148..8aeb78f 100644 --- a/.npmignore +++ b/.npmignore @@ -62,6 +62,7 @@ yarn.lock #IDE .idea .history +.vscode test bin From 5a6cf8236bd1c8501107045c88395faefae6f63b Mon Sep 17 00:00:00 2001 From: Simone Mariotti Date: Mon, 10 Jul 2017 18:00:08 +0200 Subject: [PATCH 7/7] Updated ReadMe --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 061a366..aa08c73 100644 --- a/README.md +++ b/README.md @@ -225,8 +225,7 @@ const result = new WhichBrowser(request.header, { } ); ``` -A value for `cacheExpires` less or equal to `0` disable the expiry for that result and it will last until you restart node or you parse another UA with a `cacheExpires` greater than `0`. -This will restart the cache validity check and all the records parsed with `cacheExpires <= 0` will be removed at the next cache validity check. +A value for `cacheExpires` less or equal to `0` disable the expiry for that result and it will last until you restart node or you parse the same set of headers with a `cacheExpires` greater than `0`. Cache validity is checked at a rate of `cacheExpires / 5` so, with a `cacheExpires` of `500`, you can rest assured that your result has been reaped from the cache after `500 + 500 / 5 + 1` seconds. @@ -235,13 +234,14 @@ If you want to speed up the process of validity check you can set the `cacheChec ```js const result = new WhichBrowser(request.header, { cache: WhichBrowser.SIMPLE_CACHE, + cacheExpires: 300, cacheCheckInterval: 1 } ); ``` -In this way the cache lasts for `900` seconds but is checked every `1` second. +In this way the cache lasts for `300` seconds but is checked every `1` second. -> Be aware that changing the `cacheExpiries` *doesn't* impact the expiry date of others records in the cache. However it *will change* the rate at which the cache validity is checked as it does `cacheCheckInterval`. For example setting `cacheExpiries` to `0` will prevent **ALL** results to expire because it will disable the cache validity check. +> Be aware that changing `cacheExpiries` or `cacheCheckInterval` impact the cache validity check rate for **ALL** records in the cache. For example setting `cacheExpiries` to `0` will prevent **ALL** results to expire because it will disable the cache validity check (for the sake of truth it will be done every `57085` years, `5` months, `10` days, `7` hours, `35` minutes and `48` seconds). API reference -------------