diff --git a/.gitignore b/.gitignore index c2658d7..874cbe9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ +coverage/ +index-es5.js +index-es5.js.map node_modules/ +.nyc_output/ +package-lock.json diff --git a/.travis.yml b/.travis.yml index aa5bbfb..6f3fd62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: node_js node_js: -- "0.10" -- "0.12" -- 4 -- 6 -- 8 -- node -script: npm test + - 14 + - node +script: npm run ci diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 4807e81..5842147 --- a/README.md +++ b/README.md @@ -1,11 +1,14 @@ -# urlcache [![NPM Version][npm-image]][npm-url] [![Bower Version][bower-image]][bower-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][david-image]][david-url] +# urlcache [![NPM Version][npm-image]][npm-url] ![File Size][filesize-image] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Dependency Monitor][greenkeeper-image]][greenkeeper-url] -> URL key-value cache. +> Normalized [`URL`](https://mdn.io/URL/) key-value cache. -## Installation -[Node.js](http://nodejs.org/) `>= 0.10` is required; `< 4.0` will need an `Object.assign` polyfill. To install, type this at the command line: +In an effort to prevent duplicates, unnecessary URL components will be (optionally) reduced/normalized. + + +## Installation +[Node.js](https://nodejs.org) `>= 14` is required. To install, type this at the command line: ```shell npm install urlcache ``` @@ -13,80 +16,80 @@ npm install urlcache ## Constructor ```js -var UrlCache = require("urlcache"); -var cache = new UrlCache(options); +const URLCache = require('urlcache'); +const cache = new URLCache(options); ``` -## Methods -**Note:** all instances of `url` can be either a `String` or a [`url.parse()`](https://nodejs.org/api/url.html#url_url_parse_urlstr_parsequerystring_slashesdenotehost)-compatible `Object`. +## Methods & Properties + +### `.clean()` +Removes all stored key-value pairs that have expired. + +### `.clear()` +Removes all stored key-value pairs. + +### `.delete(url)` +Removes the `url` key-value pair. -### .clear([url]) -Removes the `url` key-value pair. If the `url` argument is not defined, *all* pairs will be removed. +### `.get(url)` +Returns the stored value for `url`, or `undefined` if there is none. -### .get(url) -Returns the stored value of `url`. If no such value exists, `undefined` will be returned. +### `.has(url)` +Returns `true` if there is a stored value for `url`. -### .length() +### `.length` Returns the number of stored key-value pairs. -### .set(url, value[, expiryTime]) -Stores `value` (any type) into `url` key. Optionally, define `expiryTime` to override `options.expiryTime`. +### `.set(url, value[, options])` +Stores `value` (any type) associated with `url` key. Optionally, define `options` to override any defined in the constructor. ```js -cache.set("url", {"key":"value"}); -cache.get("url"); //=> {"key":"value"} - -cache.set("url", new Promise(function(resolve, reject) { - // set value after some delayed event - setTimeout(function() { - resolve("value"); - }, 500); -}); +const url = new URL('http://domain/'); + +cache.set(url, {'key':'value'}); +cache.get(url); //-> {'key':'value'} -Promise.resolve(cache.get("url")).then(function(value) { - console.log(value); //=> "value" +cache.set(url, new Promise(resolve => { + // set value after some delayed event + setTimeout(() => resolve('value'), 500); }); + +console.log(await cache.get(url)); //-> 'value' ``` ## Options -### options.defaultPorts +### `carefulProfile` Type: `Object` -Default value: see [urlobj.parse() options](https://github.com/stevenvachon/urlobj) -A map of protocol default ports for `options.normalizeUrls`. +Default value: see [minurl option profiles](https://npmjs.com/minurl#option-profiles) +A configuration of normalizations performed on URLs to hosts that may not be configured correctly or ideally. -### options.expiryTime +### `commonProfile` +Type: `Object` +Default value: see [minurl option profiles](https://npmjs.com/minurl#option-profiles) +A configuration of normalizations performed on URLs to hosts that you expect to be configured correctly and ideally. + +### `maxAge` Type: `Number` Default value: `Infinity` The number of milliseconds in which a cached value should be considered valid. -### options.normalizeUrls -Type: `Boolean` -Default value: `true` -When `true`, will remove unnecessary URL parts in order to avoid duplicates in cache. - -### options.stripUrlHashes -Type: `Boolean` -Default Value: `true` -When `true`, will remove `#hashes` from URLs. They are most likely not useful to you because they are local to the document that contains them. - +### `profile` +Type: `String` +Default value: `'common'` +The URL normalization profile. For example, value of `'common'` will use `commonProfile`. -## Changelog -* 0.7.0 support for Node.js v9 -* 0.6.0 added `.length()` and removed `Object.assign()` polyfill -* 0.5.0 removed use of Promises as they were unnecessary -* 0.4.0 simpler `Promise`-based API -* 0.3.0 added `options.defaultPorts`, more tests -* 0.2.0 simplified API -* 0.1.0 initial release +### Default Options +`URLCache.DEFAULT_OPTIONS` is available for customizable extension. [npm-image]: https://img.shields.io/npm/v/urlcache.svg [npm-url]: https://npmjs.org/package/urlcache -[bower-image]: https://img.shields.io/bower/v/urlcache.svg -[bower-url]: https://github.com/stevenvachon/urlcache +[filesize-image]: https://img.shields.io/badge/bundle-40kB%20gzipped-blue.svg [travis-image]: https://img.shields.io/travis/stevenvachon/urlcache.svg [travis-url]: https://travis-ci.org/stevenvachon/urlcache -[david-image]: https://img.shields.io/david/stevenvachon/urlcache.svg -[david-url]: https://david-dm.org/stevenvachon/urlcache +[coveralls-image]: https://img.shields.io/coveralls/stevenvachon/urlcache.svg +[coveralls-url]: https://coveralls.io/github/stevenvachon/urlcache +[greenkeeper-image]: https://badges.greenkeeper.io/stevenvachon/urlcache.svg +[greenkeeper-url]: https://greenkeeper.io/ diff --git a/bower.json b/bower.json deleted file mode 100644 index 8d3474a..0000000 --- a/bower.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "urlcache", - "homepage": "https://github.com/stevenvachon/urlcache", - "license": "MIT", - "authors": [ - "Steven Vachon " - ], - "description": "URL key-value cache and store.", - "main": "index-browser.js", - "keywords": [ - "cache", - "uri", - "url" - ], - "ignore": [ - "bower_components", - "index.js", - "node_modules", - "package.json", - "test.js" - ] -} diff --git a/index-browser.js b/index-browser.js deleted file mode 100644 index e5c8603..0000000 --- a/index-browser.js +++ /dev/null @@ -1 +0,0 @@ -!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).UrlCache=t()}}(function(){return function s(a,i,h){function l(r,t){if(!i[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(u)return u(r,!0);var n=new Error("Cannot find module '"+r+"'");throw n.code="MODULE_NOT_FOUND",n}var o=i[r]={exports:{}};a[r][0].call(o.exports,function(t){var e=a[r][1][t];return l(e||t)},o,o.exports,s,a,i,h)}return i[r].exports}for(var u="function"==typeof require&&require,t=0;t= 0x80 (not a basic code point)","invalid-input":"Invalid input"},p=g-v,j=Math.floor,I=String.fromCharCode;function S(t){throw new RangeError(c[t])}function f(t,e){for(var r=t.length,n=[];r--;)n[r]=e(t[r]);return n}function d(t,e){var r=t.split("@"),n="";return 1>>10&1023|55296),t=56320|1023&t),e+=I(t)}).join("")}function R(t,e){return t+22+75*(t<26)-((0!=e)<<5)}function L(t,e,r){var n=0;for(t=r?j(t/i):t>>1,t+=j(t/e);p*O>>1j((x-d)/a))&&S("overflow"),d+=h*a,!(h<(l=i<=m?v:m+O<=i?O:i-m));i+=g)a>j(x/(u=g-l))&&S("overflow"),a*=u;m=L(d-s,e=p.length+1,0==s),j(d/e)>x-y&&S("overflow"),y+=j(d/e),d%=e,p.splice(d++,0,y)}return A(p)}function m(t){var e,r,n,o,s,a,i,h,l,u,c,p,f,d,y,m=[];for(p=(t=U(t)).length,e=E,s=b,a=r=0;aj((x-r)/(f=n+1))&&S("overflow"),r+=(i-e)*f,e=i,a=0;ax&&S("overflow"),c==e){for(h=r,l=g;!(h<(u=l<=s?v:s+O<=l?O:l-s));l+=g)y=h-u,d=g-u,m.push(I(R(u+y%d,0))),h=j(y/d);m.push(I(R(h,0))),s=L(r,f,n==o),r=0,++n}++r,++e}return m.join("")}if(o={version:"1.4.1",ucs2:{decode:U,encode:A},decode:y,encode:m,toASCII:function(t){return d(t,function(t){return l.test(t)?"xn--"+m(t):t})},toUnicode:function(t){return d(t,function(t){return h.test(t)?y(t.slice(4).toLowerCase()):t})}},e&&r)if(D.exports==e)r.exports=o;else for(s in o)o.hasOwnProperty(s)&&(e[s]=o[s]);else t.punycode=o}(this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],6:[function(t,e,r){"use strict";e.exports=function(t,e,r,n){e=e||"&",r=r||"=";var o={};if("string"!=typeof t||0===t.length)return o;var s=/\+/g;t=t.split(e);var a=1e3;n&&"number"==typeof n.maxKeys&&(a=n.maxKeys);var i,h,l=t.length;0",'"',"`"," ","\r","\n","\t"]),N=["'"].concat(o),_=["%","/","?",";","#"].concat(N),z=["/","?","#"],M=/^[+a-z0-9A-Z_-]{0,63}$/,V=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,Y={javascript:!0,"javascript:":!0},Q={javascript:!0,"javascript:":!0},F={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},k=t("querystring");function s(t,e,r){if(t&&q.isObject(t)&&t instanceof U)return t;var n=new U;return n.parse(t,e,r),n}U.prototype.parse=function(t,e,r){if(!q.isString(t))throw new TypeError("Parameter 'url' must be a string, not "+typeof t);var n=t.indexOf("?"),o=-1!==n&&n=w.PROTOCOL_RELATIVE&&(a.protocol=s.protocol,a.slashes=s.slashes,a.extra.protocolTruncated=s.extra.protocolTruncated,!0))&&(i=t,O=(h=e).extra.type>=w.ROOT_RELATIVE&&(h.auth=i.auth,h.host=i.host,h.port=i.port,h.hostname=i.hostname,h.extra.portIsDefault=i.extra.portIsDefault,!0)),o=w.FILENAME_RELATIVE&&(u.extra.directory=l.extra.directory.slice(),u.extra.directoryLeadingSlash=l.extra.directoryLeadingSlash,!0)),(o>=C.DIRECTORY||!0===n)&&(m=t,v=(x=e).extra.type>=w.QUERY_RELATIVE&&(x.extra.filename=m.extra.filename,x.extra.filenameIsIndex=m.extra.filenameIsIndex,!0),d=t,E=(y=e).extra.type>=w.EMPTY&&(y.search=d.search,!0===q(d.query)?(y.query=P({},d.query),y.extra.query=y.query):(y.query=d.query,y.extra.query=P({},d.extra.query)),!0)),!0!==g&&!0!==v||(e.pathname=I(e)),!0!==g&&!0!==v&&!0!==E||(e.path=j(e)),!0===b&&!1===O&&(e.extra.portIsDefault=S(e.protocol,e.port,r.defaultPorts)),!0!==b&&!0!==O&&!0!==g&&!0!==v&&!0!==E||(e.href=H.format(e),e.extra.type=L(e))),e}},{"./UrlComponent":12,"./UrlType":13,"./areSameDir":14,"./formatPath":16,"./formatPathname":17,"./isDefaultPort":20,"./normalizeDirs":23,"./parseUrl":26,"./resolveDirs":27,"./typeofUrl":29,"./urlRelation":30,"is-object":2,"object-assign":4,url:9}],29:[function(t,e,r){"use strict";var n=t("./UrlType");e.exports=function(t){return null!==t.protocol?n.ABSOLUTE:null!==t.hostname?n.PROTOCOL_RELATIVE:!0===t.extra.directoryLeadingSlash?n.ROOT_RELATIVE:0 { - this.options = Object.assign({}, defaultOptions, options); - - this.clear(); -} + const profileName = `${ customOptions.profile ?? instanceOptions.profile }Profile`; + const profile = customOptions[profileName] ?? instanceOptions[profileName]; + + return minURL(url, profile); +}; -UrlCache.prototype.clear = function(url) +const remove = (instance, url) => { - if (url != null) - { - url = parseUrl(url, this.options); - url = stringifyUrl(url); - - if (this.values[url] !== undefined) - { - delete this.expiries[url]; - delete this.values[url]; - - this.count--; - } - } - else + if (url in instance.values) { - this.count = 0; - this.expiries = {}; - this.values = {}; + delete instance.ages[url]; + delete instance.values[url]; + + instance.count--; } }; -UrlCache.prototype.get = function(url) +const removeExpired = (instance, url) => { - url = formatUrl(url, this.options); - - removeExpired(url, this.expiries, this.values); - - return this.values[url]; + if (instance.ages[url] < Date.now()) + { + remove(instance, url); + } }; -UrlCache.prototype.length = function() +class URLCache { - return this.count; -}; - - + constructor(options) + { + this.options = { ...DEFAULT_OPTIONS, ...options }; -UrlCache.prototype.set = function(url, value, expiryTime) -{ - // Avoid filling cache with values that will only cause rejection - if (value === undefined) return; - - url = formatUrl(url, this.options); - - if (expiryTime == null) expiryTime = this.options.expiryTime; - - this.expiries[url] = Date.now() + expiryTime; - this.values[url] = value; - - this.count++; -}; + this.clear(); + } -//::: PRIVATE FUNCTIONS + clean() + { + Object.keys(this.ages).forEach(url => removeExpired(this, url)); + } -function formatUrl(url, options) -{ - url = parseUrl(url, options); - url = stringifyUrl(url); - - return url; -} + clear() + { + this.ages = {}; + this.count = 0; + this.values = {}; + } -function parseUrl(url, options) -{ - if (options.defaultPorts != null) + delete(url) { - url = urlobj.parse(url, {defaultPorts:options.defaultPorts}); + remove(this, formatURL(url, this.options)); } - else + + + + get(url) { - // Avoid overriding the default value of `defaultPorts` with null/undefined - url = urlobj.parse(url); + url = formatURL(url, this.options); + + removeExpired(this, url); + + return this.values[url]; } - - if (options.normalizeUrls === true) + + + + has(url) { - // TODO :: this mutates input - urlobj.normalize(url); + url = formatURL(url, this.options); + + removeExpired(this, url); + + return url in this.values; } - - if (options.stripUrlHashes===true && url.hash!=null) + + + + get length() { - // TODO :: this mutates input - url.hash = null; - url.href = stringifyUrl(url); + return this.count; } - - return url; -} -function removeExpired(url, expiries, values) -{ - if (values[url] !== undefined) + // @todo `url, headers, value, options` ... key can be a hash from url and headers + // see https://github.com/ForbesLindesay/http-basic/issues/24#issuecomment-293630902 + set(url, value, options={}) { - if ( expiries[url] < Date.now() ) + url = formatURL(url, this.options, options); + + if (!(url in this.values)) { - delete expiries[url]; - delete values[url]; + this.count++; } + + this.ages[url] = Date.now() + (options.maxAge ?? this.options.maxAge); + this.values[url] = value; } } -function stringifyUrl(url) -{ - if (url!==null && typeof url==="object" && url instanceof String===false) - { - return urllib.format(url); // TODO :: use urlobj.format() when available - } -} +URLCache.DEFAULT_OPTIONS = DEFAULT_OPTIONS; -module.exports = UrlCache; +module.exports = deepFreeze(URLCache); diff --git a/package.json b/package.json index 260e25a..3f587dc 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,48 @@ { "name": "urlcache", - "description": "URL key-value cache.", - "version": "0.7.0", + "description": "Normalized URL key-value cache.", + "version": "1.0.0-alpha", "license": "MIT", - "author": "Steven Vachon (https://www.svachon.com/)", - "repository": "stevenvachon/urlcache", + "author": "Steven Vachon (https://svachon.com)", + "repository": "github:stevenvachon/urlcache", + "browser": "index-es5.js", "dependencies": { - "urlobj": "0.0.11" + "deep-freeze-node": "^1.1.3", + "minurl": "github:stevenvachon/minurl" }, "devDependencies": { - "bhttp": "^1.2.4", - "browserify": "^16.1.1", - "chai": "^3.5.0", - "chai-as-promised": "^5.2.0", - "es6-promise": "^3.3.1", - "mocha": "^3.5.3", - "object.assign": "^4.1.0", - "uglify-js": "^3.3.14" + "@babel/cli": "^7.13.0", + "@babel/core": "^7.13.8", + "@babel/preset-env": "^7.13.8", + "babelify": "^10.0.0", + "browserify": "^17.0.0", + "c8": "^7.6.0", + "chai": "^4.3.0", + "chai-as-promised": "^7.1.1", + "coveralls": "^3.1.0", + "gzip-size-cli": "^4.0.0", + "mocha": "^8.3.0", + "terser": "^5.6.0", + "universal-url": "^2.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 14" }, "scripts": { - "browserify": "browserify index.js --standalone UrlCache | uglifyjs --compress --mangle -o index-browser.js", - "test": "mocha test.js --reporter spec --check-leaks --bail", - "test-watch": "mocha test.js --reporter spec --check-leaks --bail -w" + "ci": "npm test && c8 report --reporter=text-lcov | coveralls", + "posttest": "c8 report --reporter=text-summary --reporter=html && browserify index.js --global-transform [ babelify --presets [ @babel/env ] ] --standalone=URLCache | terser --compress --mangle | gzip-size", + "prepublishOnly": "npm test && babel index.js --out-file=index-es5.js --presets=@babel/env --source-maps", + "test": "c8 mocha test.js --bail --check-leaks" }, "files": [ "index.js", - "license" + "index-es5.js", + "index-es5.js.map" ], "keywords": [ "cache", "uri", - "url" + "url", + "whatwg" ] } diff --git a/test.js b/test.js old mode 100644 new mode 100755 index 73e7d49..f2a5506 --- a/test.js +++ b/test.js @@ -1,385 +1,507 @@ "use strict"; -var UrlCache = require("./"); +const {describe, it} = require("mocha"); +const {expect} = require("chai").use( require("chai-as-promised") ); +const {URL} = require("universal-url"); +const URLCache = require("./"); -var chai = require("chai"); -var urllib = require("url"); -var expect = chai.expect; -chai.use( require("chai-as-promised") ); -require("es6-promise").polyfill(); -require("object.assign").shim(); +const options = overrides => +({ + carefulProfile: URLCache.DEFAULT_OPTIONS.carefulProfile, + commonProfile: URLCache.DEFAULT_OPTIONS.commonProfile, + maxAge: Infinity, + profile: "common", + ...overrides +}); -function options(overrides) +describe("DEFAULT_OPTIONS", () => { - var testDefaults = + it("is publicly available", () => { - // All others will use default values - // as this will ensure that when they change, tests WILL break - normalizeUrls: false, - stripUrlHashes: false - }; - - return Object.assign({}, testDefaults, overrides); -} + expect( URLCache.DEFAULT_OPTIONS ).to.be.an("object"); + + const originalValue = URLCache.DEFAULT_OPTIONS; + + expect(() => URLCache.DEFAULT_OPTIONS = "changed").to.throw(Error); + expect(() => URLCache.DEFAULT_OPTIONS.careful = "changed").to.throw(Error); + expect(URLCache.DEFAULT_OPTIONS).to.equal(originalValue); + }); +}); -describe("get()", function() +describe("has()", () => { - it("should get a value", function() + it("determines if a value exists", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = "another-url"; - + const cache = new URLCache( options() ); + let url1 = "http://some-url"; + let url2 = "http://another-url"; + // Sets internal value - cache.expiries[url1] = Infinity; - cache.expiries[url2] = Infinity; + cache.ages[url1] = Infinity; + cache.ages[url2] = Infinity; cache.values[url1] = 1; cache.values[url2] = 2; - - url2 = urllib.parse(url2); - + + url1 = new URL(url1); + url2 = new URL(url2); + + expect( cache.has(url1) ).to.be.true; + expect( cache.has(url2) ).to.be.true; + }); + + + + it("determines if no value exists", () => + { + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + + expect( cache.has(url1) ).to.be.false; + expect( cache.has(url2) ).to.be.false; + }); + + + + it("rejects non-URL keys", () => + { + const cache = new URLCache( options() ); + expect(() => cache.has("url")).to.throw(TypeError); + expect(() => cache.has({})).to.throw(TypeError); + }); +}); + + + +describe("get()", () => +{ + it("gets a value", () => + { + const cache = new URLCache( options() ); + let url1 = "http://some-url"; + let url2 = "http://another-url"; + + // Sets internal value + cache.ages[url1] = Infinity; + cache.ages[url2] = Infinity; + cache.values[url1] = 1; + cache.values[url2] = undefined; + + url1 = new URL(url1); + url2 = new URL(url2); + expect( cache.get(url1) ).to.equal(1); - expect( cache.get(url2) ).to.equal(2); + expect( cache.get(url2) ).to.be.undefined; }); - - - - it("should support values containing Promises", function() + + + + it("gets no value if none exists", () => + { + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + + expect( cache.get(url1) ).to.be.undefined; + expect( cache.get(url2) ).to.be.undefined; + }); + + + + it("rejects non-URL keys", () => + { + const cache = new URLCache( options() ); + expect(() => cache.get("url")).to.throw(TypeError); + expect(() => cache.get({})).to.throw(TypeError); + }); + + + + it("supports values containing Promises", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = "another-url"; - var value1 = new Promise(function(resolve,reject){ setImmediate(function(){ resolve(1) }) }); - var value2 = new Promise(function(resolve,reject){ setImmediate(function(){ reject() }) }); - + const cache = new URLCache( options() ); + let url1 = "http://some-url"; + let url2 = "http://another-url"; + let value1 = new Promise(function(resolve,reject){ setImmediate(() => resolve(1) ) }); + let value2 = new Promise(function(resolve,reject){ setImmediate(() => reject() ) }); + // Sets internal value - cache.expiries[url1] = Infinity; - cache.expiries[url2] = Infinity; + cache.ages[url1] = Infinity; + cache.ages[url2] = Infinity; cache.values[url1] = value1; cache.values[url2] = value2; - - url2 = urllib.parse(url2); - + + url1 = new URL(url1); + url2 = new URL(url2); + expect( cache.get(url1) ).to.eventually.equal(1); expect( cache.get(url2) ).to.be.rejected; }); - - - - it("should reject a key with no value", function() - { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = urllib.parse("another-url"); - - expect( cache.get(url1) ).to.be.undefined; - expect( cache.get(url2) ).to.be.undefined; - }); }); -// NOTE :: `expiryTime` is tested in "options" area -describe("set()", function() +// NOTE :: option overrides are tested in "options" area +describe("set()", () => { - it("should get what was set", function() + it("sets a value", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = urllib.parse("another-url"); - + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + cache.set(url1, "some value"); cache.set(url2, "another value"); - + expect( cache.get(url1) ).to.equal("some value"); expect( cache.get(url2) ).to.equal("another value"); }); - - - - it("should overwrite a value", function() + + + + it("overwrites a value", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = urllib.parse("another-url"); - var value1 = "some value"; - var value2 = "another value"; - + const cache = new URLCache( options() ); + let url1 = new URL("http://some-url"); + let url2 = new URL("http://another-url"); + let value1 = "some value"; + let value2 = "another value"; + cache.set(url1, value1); cache.set(url2, value2); expect( cache.get(url1) ).to.equal(value1); expect( cache.get(url2) ).to.equal(value2); - + value1 = "some other value"; value2 = "yet another value"; - + cache.set(url1, value1); cache.set(url2, value2); expect( cache.get(url1) ).to.equal(value1); expect( cache.get(url2) ).to.equal(value2); }); + + + + it("rejects non-URL keys", () => + { + const cache = new URLCache( options() ); + expect(() => cache.set("url", "value")).to.throw(TypeError); + expect(() => cache.set({}, "value")).to.throw(TypeError); + }); }); -describe("clear()", function() +describe("delete()", () => { - it("should work specifically", function() + it("removes a key-value pair", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = urllib.parse("another-url"); - + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + cache.set(url1, "some value"); cache.set(url2, "another value"); - cache.clear(url1); - cache.clear(url2); - - expect( cache.get(url1) ).to.be.undefined; - expect( cache.get(url2) ).to.be.undefined; + cache.delete(url1); + cache.delete(url2); + + expect( cache.has(url1) ).to.be.false; + expect( cache.has(url2) ).to.be.false; + }); + + + + it("rejects non-URL keys", () => + { + const cache = new URLCache( options() ); + expect(() => cache.delete("url")).to.throw(TypeError); + expect(() => cache.delete({})).to.throw(TypeError); }); - - - - it("should work globally", function() +}); + + + +describe("clean()", () => +{ + it("removes expired key-value pairs", () => { - var cache = new UrlCache( options() ); - var url1 = "some-url"; - var url2 = urllib.parse("another-url"); - + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + + cache.set(url1, "some value"); + cache.set(url2, "another value", { maxAge:-1 }); + cache.clean(); + + expect( cache.has(url1) ).to.be.true; + expect( cache.has(url2) ).to.be.false; + }); +}); + + + +describe("clear()", () => +{ + it("removes all key-value pairs", () => + { + const cache = new URLCache( options() ); + const url1 = new URL("http://some-url"); + const url2 = new URL("http://another-url"); + cache.set(url1, "some value"); cache.set(url2, "another value"); cache.clear(); - - expect( cache.get(url1) ).to.be.undefined; - expect( cache.get(url2) ).to.be.undefined; + + expect( cache.has(url1) ).to.be.false; + expect( cache.has(url2) ).to.be.false; }); }); -describe("length()", function() +describe("length", () => { - it("should work", function() + it("works", () => { - var cache = new UrlCache( options() ); - - expect(cache.length()).to.equal(0); - + const cache = new URLCache( options() ); + + expect(cache).to.have.length(0); + + cache.clean(); + expect(cache).to.have.length(0); + cache.clear(); - expect(cache.length()).to.equal(0); - - cache.clear("not-available"); - expect(cache.length()).to.equal(0); - - cache.set("some-url1", "some value1"); - expect(cache.length()).to.equal(1); - - cache.set("some-url2", "some value2"); - cache.set("some-url3", "some value3"); - expect(cache.length()).to.equal(3); - - cache.clear("not-available"); - expect(cache.length()).to.equal(3); - - cache.clear("some-url3"); - expect(cache.length()).to.equal(2); - + expect(cache).to.have.length(0); + + cache.delete( new URL("http://not-available") ); + expect(cache).to.have.length(0); + + cache.set( new URL("http://some-url1"), "some value1"); + expect(cache).to.have.length(1); + + cache.set( new URL("http://some-url1"), "new value"); + expect(cache).to.have.length(1); + + cache.set( new URL("http://some-url2"), "some value2"); + cache.set( new URL("http://some-url3"), "some value3"); + cache.set( new URL("http://some-url4"), "some value4", { maxAge:-1 }); + expect(cache).to.have.length(4); + + cache.delete( new URL("http://not-available") ); + expect(cache).to.have.length(4); + + cache.delete( new URL("http://some-url3") ); + expect(cache).to.have.length(3); + + cache.clean(); + expect(cache).to.have.length(2); + cache.clear(); - expect(cache.length()).to.equal(0); + expect(cache).to.have.length(0); }); }); -describe("options", function() +describe("options", () => { - it("expiryTime = 50", function(done) + it("maxAge = 50", done => { - var cache = new UrlCache( options({ expiryTime:50 }) ); - var url = "some-url"; - var value = "some value"; - + const cache = new URLCache( options({ maxAge:50 }) ); + const url = new URL("http://some-url"); + const value = "some value"; + cache.set(url, value); - + + expect( cache.has(url) ).to.be.true; expect( cache.get(url) ).to.equal(value); - - setTimeout( function() + expect( cache ).to.have.length(1); + + setTimeout(() => { + expect( cache.has(url) ).to.be.false; expect( cache.get(url) ).to.be.undefined; - + expect( cache ).to.have.length(0); + cache.set(url, value); - + + expect( cache.has(url) ).to.be.true; expect( cache.get(url) ).to.equal(value); - - setTimeout( function() + expect( cache ).to.have.length(1); + + setTimeout(() => { + expect( cache.has(url) ).to.be.false; expect( cache.get(url) ).to.be.undefined; - - setTimeout(function(){ done() }, 50); - + expect( cache ).to.have.length(0); + done(); + }, 100); - + }, 100); }); - - - - it("expiryTime = 50 (specific override)", function(done) + + + + it("maxAge = 50 (specific override)", done => { - var cache = new UrlCache( options({ expiryTime:500 }) ); - var url = "some-url"; - var value = "some value"; - - cache.set(url, value, 50); - + const cache = new URLCache( options({ maxAge:500 }) ); + const url = new URL("http://some-url"); + const value = "some value"; + + cache.set(url, value, { maxAge:50 }); + + expect( cache.has(url) ).to.be.true; expect( cache.get(url) ).to.equal(value); - - setTimeout( function() + expect( cache ).to.have.length(1); + + setTimeout(() => { + expect( cache.has(url) ).to.be.false; expect( cache.get(url) ).to.be.undefined; - - cache.set(url, value, 50); - + expect( cache ).to.have.length(0); + + cache.set(url, value, { maxAge:50 }); + + expect( cache.has(url) ).to.be.true; expect( cache.get(url) ).to.equal(value); - - setTimeout( function() + expect( cache ).to.have.length(1); + + setTimeout(() => { + expect( cache.has(url) ).to.be.false; expect( cache.get(url) ).to.be.undefined; + expect( cache ).to.have.length(0); done(); - + }, 100); - + }, 100); }); - - - - it("normalizeUrls = false", function(done) - { - var cache = new UrlCache( options() ); - var url1 = "http://domain.com/path/../to/something/../index.html"; - var url2 = "http://domain.com/to/index.html"; - var value1 = "some value"; - var value2 = "another value"; - - cache.set(url1, value1); - - expect( cache.get(url1) ).to.equal(value1); - - setTimeout( function() - { - cache.set(url2, value2); - - // Has not been overwritten - expect( cache.get(url1) ).to.equal(value1); - done(); - - }, 50); - }); - - - - it("normalizeUrls = true", function(done) + + + + it(`profile = "common"`, done => { - var cache = new UrlCache( options({ normalizeUrls:true }) ); - var url1 = "http://domain.com/path/../to/something/../index.html"; - var url2 = "http://domain.com/to/index.html"; - var value1 = "some value"; - var value2 = "another value"; - + const cache = new URLCache( options() ); + const url1 = new URL("http://domain.com/path/to/index.html?#"); + const url2 = new URL("http://domain.com/path/to/"); + const value1 = "some value"; + const value2 = "another value"; + cache.set(url1, value1); - + expect( cache.get(url1) ).to.equal(value1); - - setTimeout( function() + + setTimeout(() => { cache.set(url2, value2); - + // Has been overwritten expect( cache.get(url1) ).to.equal(value2); done(); - + }, 50); }); - - - - it("normalizeUrls = true, defaultPorts = {…}", function(done) + + + + it(`profile = "careful"`, done => { - var cache = new UrlCache( options({ defaultPorts:{protocol:1234}, normalizeUrls:true }) ); - var url1 = "protocol://domain.com:1234/path/to/"; - var url2 = "protocol://domain.com/path/to/"; - var value1 = "some value"; - var value2 = "another value"; - + const cache = new URLCache( options({ profile:"careful" }) ); + const url1 = new URL("http://domain.com/path/to/index.html?#"); + const url2 = new URL("http://domain.com/path/to/"); + const value1 = "some value"; + const value2 = "another value"; + cache.set(url1, value1); - + expect( cache.get(url1) ).to.equal(value1); - - setTimeout( function() + + setTimeout(() => { cache.set(url2, value2); - - // Has been overwritten - expect( cache.get(url1) ).to.equal(value2); + + // Has not been overwritten + expect( cache.get(url1) ).to.equal(value1); done(); - + }, 50); }); - - - - it("stripUrlHashes = false", function(done) + + + + it("carefulProfile = {…}", done => { - var cache = new UrlCache( options() ); - var url1 = "http://domain.com/path/to/#hash"; - var url2 = "http://domain.com/path/to/"; - var value1 = "some value"; - var value2 = "another value"; - - cache.set(url1, value1); - - expect( cache.get(url1) ).to.equal(value1); - - setTimeout( function() + const cache = new URLCache( options({ profile:"careful", removeHash:false, carefulProfile:{ defaultPorts:{"protocol:":1234} } }) ); + const url_a1 = new URL("protocol://domain.com:1234/path/"); + const url_a2 = new URL("protocol://domain.com/path/"); + const url_b1 = new URL("http://domain.com/#hash"); + const url_b2 = new URL("http://domain.com/"); + const value1 = "some value"; + const value2 = "another value"; + + cache.set(url_a1, value1); + cache.set(url_b1, value1); + + expect( cache.get(url_a1) ).to.equal(value1); + expect( cache.get(url_b1) ).to.equal(value1); + + setTimeout(() => { - cache.set(url2, value2); - + cache.set(url_a2, value2); + cache.set(url_b2, value2); + + // Has been overwritten + expect( cache.get(url_a1) ).to.equal(value2); + // Has not been overwritten - expect( cache.get(url1) ).to.equal(value1); + expect( cache.get(url_b1) ).to.equal(value1); + expect( cache.get(url_b2) ).to.equal(value2); + done(); - + }, 50); }); - - - - it("stripUrlHashes = true", function(done) + + + + it("commonProfile = {…}", done => { - var cache = new UrlCache( options({ stripUrlHashes:true }) ); - var url1 = "http://domain.com/path/to/#hash"; - var url2 = "http://domain.com/path/to/"; - var value1 = "some value"; - var value2 = "another value"; - - cache.set(url1, value1); - - expect( cache.get(url1) ).to.equal(value1); - - setTimeout( function() + const cache = new URLCache( options({ removeHash:false, commonProfile:{ defaultPorts:{"protocol:":1234} } }) ); + const url_a1 = new URL("protocol://domain.com:1234/path/"); + const url_a2 = new URL("protocol://domain.com/path/"); + const url_b1 = new URL("http://domain.com/#hash"); + const url_b2 = new URL("http://domain.com/"); + const value1 = "some value"; + const value2 = "another value"; + + cache.set(url_a1, value1); + cache.set(url_b1, value1); + + expect( cache.get(url_a1) ).to.equal(value1); + expect( cache.get(url_b1) ).to.equal(value1); + + setTimeout(() => { - cache.set(url2, value2); - + cache.set(url_a2, value2); + cache.set(url_b2, value2); + // Has been overwritten - expect( cache.get(url1) ).to.equal(value2); + expect( cache.get(url_a1) ).to.equal(value2); + + // Has not been overwritten + expect( cache.get(url_b1) ).to.equal(value1); + expect( cache.get(url_b2) ).to.equal(value2); + done(); - + }, 50); }); });