diff --git a/README.md b/README.md index 7a1d753..e468c40 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,9 @@ -![Logo](https://user-images.githubusercontent.com/15038724/94977866-9e915c80-04cf-11eb-9f4f-fd3bcf5c8a54.png) - -# FireFerret -Autocaching query client for MongoDB, with powerful filtering functionality. - -_We care about response times!_ +![Logo](https://user-images.githubusercontent.com/15038724/94977866-9e915c80-04cf-11eb-9f4f-fd3bcf5c8a54.png) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/licenses/MIT) -[![JavaScript Style Guide: Standard](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com/ "JavaScript Standard Style") -[![Build Status](https://travis-ci.com/mster/fireferret.svg?branch=master)](https://travis-ci.com/mster/fireferret) -[![Coverage Status](https://coveralls.io/repos/github/mster/fireferret/badge.svg?branch=master)](https://coveralls.io/github/mster/fireferret?branch=master) +# FireFerret [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/licenses/MIT) [![JavaScript Style Guide: Standard](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com/ "JavaScript Standard Style") [![Build Status](https://travis-ci.com/mster/fireferret.svg?branch=master)](https://travis-ci.com/mster/fireferret) [![Coverage Status](https://coveralls.io/repos/github/mster/fireferret/badge.svg?branch=master)](https://coveralls.io/github/mster/fireferret?branch=master) -[![NPM](https://nodei.co/npm/fireferret.png)](https://nodei.co/npm/fireferret/) +_Node.js Read-through cache for MongoDB_. ## References @@ -23,68 +15,52 @@ _We care about response times!_ | MongoDB | [https://www.mongodb.com/](https://www.mongodb.com/) | | Redis | [https://redis.io/](https://redis.io/) | -## Requirements - -FireFerret requires MongoDB and Redis instances. ## Usage +Configure a FireFerret client by suppling a datastore and MongoDB connection information. + +To learn how to configure a datastore, see the [Datastores](#Datastores) section. + ```js -const FireFerret = require("fireferret"); +const FireFerretClient = require("fireferret"); + +const cacheConfig = { + store: require('cache-manager-redis-store'), + host: 'localhost', + port: 6379, + ... +} -const ferret = new FireFerret({ - mongo: { uri: "...", collectionName: "..." }, - redis: { host: "...", port: 6379, auth_pass: "..." }, -}); +const ferret = new FireFerretClient({ + uri: "mongodb://endpoint:27017/?compressors=zlib", + collection: "DefaultCollection", + ...cacheConfig +}) await ferret.connect(); + const docs = await ferret.fetch({ "some.field": /.*/ }); ``` Query some documents using pagination. ```js -const docs = await ferret.fetch( - { genre: { $in: ["Djent", "Math Metal"] } }, - { pagination: { page: 3, size: 20 } } -); -``` +const query = { genre: { $in: ["Djent", "Tech Death"] } }; -FireFerret supports streaming queries. +const pageOne = await ferret.fetch( + query, + { pg: [1, 20] } +); -```js -await ferret.fetch({ isOpen: true }, { stream: true }).pipe(res); +const pageTwo = await ferret.fetch( + query, + { pg: [2, 20] } +); ``` -Using the Wide-Match strategy. +## Datastores -```js -const smartFerret = new FireFerret({ - /* ... ,*/ - wideMatch: true, -}); -await smartFerret.connect(); - -const query = { candidates: { $ne: "Drumpf", $exists: true } }; - -/* cache miss */ -const first50docs = await smartFerret.fetch(query, { - pagination: { page: 1, size: 50 }, -}); - -/* cache hit */ -const first20docs = await smartFerret.fetch(query, { - pagination: { page: 1, size: 20 }, -}); - -/* cache hit */ -const first10docs = await smartFerret.fetch(query, { - pagination: { page: 1, size: 10 }, -}); - -/* cache hit */ -const firstDoc = await smartFerret.fetchOne(query); -``` ## Contributing diff --git a/docs/scripts/jaguar.js b/docs/scripts/jaguar.js index 466e2c4..a6f719f 100644 --- a/docs/scripts/jaguar.js +++ b/docs/scripts/jaguar.js @@ -1 +1 @@ -(function(){var e=0;var t;var n=document.getElementById("source-code");if(n){var i=config.linenums;if(i){n=n.getElementsByTagName("ol")[0];t=Array.prototype.slice.apply(n.children);t=t.map(function(t){e++;t.id="line"+e})}else{n=n.getElementsByTagName("code")[0];t=n.innerHTML.split("\n");t=t.map(function(t){e++;return''+t});n.innerHTML=t.join("\n")}}})();$(function(){$("#search").on("keyup",function(e){var t=$(this).val();var n=$(".navigation");if(t){var i=new RegExp(t,"i");n.find("li, .itemMembers").hide();n.find("li").each(function(e,t){var n=$(t);if(n.data("name")&&i.test(n.data("name"))){n.show();n.closest(".itemMembers").show();n.closest(".item").show()}})}else{n.find(".item, .itemMembers").show()}n.find(".list").scrollTop(0)});$(".navigation").on("click",".title",function(e){$(this).parent().find(".itemMembers").toggle()});var e=$(".page-title").data("filename").replace(/\.[a-z]+$/,"");var t=$('.navigation .item[data-name*="'+e+'"]:eq(0)');if(t.length){t.remove().prependTo(".navigation .list").show().find(".itemMembers").show()}var n=function(){var e=$(window).height();var t=$(".navigation");t.height(e).find(".list").height(e-133)};$(window).on("resize",n);n();if(config.disqus){$(window).on("load",function(){var e=config.disqus;var t=document.createElement("script");t.type="text/javascript";t.async=true;t.src="http://"+e+".disqus.com/embed.js";(document.getElementsByTagName("head")[0]||document.getElementsByTagName("body")[0]).appendChild(t);var n=document.createElement("script");n.async=true;n.type="text/javascript";n.src="http://"+e+".disqus.com/count.js";document.getElementsByTagName("BODY")[0].appendChild(n)})}}); \ No newline at end of file +(function () { let e = 0; let t; let n = document.getElementById('source-code'); if (n) { const i = config.linenums; if (i) { n = n.getElementsByTagName('ol')[0]; t = Array.prototype.slice.apply(n.children); t = t.map(function (t) { e++; t.id = 'line' + e }) } else { n = n.getElementsByTagName('code')[0]; t = n.innerHTML.split('\n'); t = t.map(function (t) { e++; return '' + t }); n.innerHTML = t.join('\n') } } })(); $(function () { $('#search').on('keyup', function (e) { const t = $(this).val(); const n = $('.navigation'); if (t) { const i = new RegExp(t, 'i'); n.find('li, .itemMembers').hide(); n.find('li').each(function (e, t) { const n = $(t); if (n.data('name') && i.test(n.data('name'))) { n.show(); n.closest('.itemMembers').show(); n.closest('.item').show() } }) } else { n.find('.item, .itemMembers').show() }n.find('.list').scrollTop(0) }); $('.navigation').on('click', '.title', function (e) { $(this).parent().find('.itemMembers').toggle() }); const e = $('.page-title').data('filename').replace(/\.[a-z]+$/, ''); const t = $('.navigation .item[data-name*="' + e + '"]:eq(0)'); if (t.length) { t.remove().prependTo('.navigation .list').show().find('.itemMembers').show() } const n = function () { const e = $(window).height(); const t = $('.navigation'); t.height(e).find('.list').height(e - 133) }; $(window).on('resize', n); n(); if (config.disqus) { $(window).on('load', function () { const e = config.disqus; const t = document.createElement('script'); t.type = 'text/javascript'; t.async = true; t.src = 'http://' + e + '.disqus.com/embed.js'; (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(t); const n = document.createElement('script'); n.async = true; n.type = 'text/javascript'; n.src = 'http://' + e + '.disqus.com/count.js'; document.getElementsByTagName('BODY')[0].appendChild(n) }) } }) diff --git a/docs/scripts/prettify/lang-css.js b/docs/scripts/prettify/lang-css.js index 041e1f5..5965904 100644 --- a/docs/scripts/prettify/lang-css.js +++ b/docs/scripts/prettify/lang-css.js @@ -1,2 +1,2 @@ -PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", -/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); +PR.registerLangHandler(PR.createSimpleLexer([['pln', /^[\t\n\f\r ]+/, null, ' \t\r\n ']], [['str', /^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/, null], ['str', /^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/, null], ['lang-css-str', /^url\(([^"')]*)\)/i], ['kwd', /^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i, null], ['lang-css-kw', /^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i], ['com', /^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//], ['com', + /^(?:<\!--|--\>)/], ['lit', /^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i], ['lit', /^#[\da-f]{3,6}/i], ['pln', /^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i], ['pun', /^[^\s\w"']+/]]), ['css']); PR.registerLangHandler(PR.createSimpleLexer([], [['kwd', /^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]), ['css-kw']); PR.registerLangHandler(PR.createSimpleLexer([], [['str', /^[^"')]+/]]), ['css-str']) diff --git a/docs/scripts/prettify/prettify.js b/docs/scripts/prettify/prettify.js index eef5ad7..00ee097 100644 --- a/docs/scripts/prettify/prettify.js +++ b/docs/scripts/prettify/prettify.js @@ -1,28 +1,108 @@ -var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; -(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= -[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), -l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, -q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, -q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, -"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), -a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} -for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], -"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], -H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], -J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ -I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), -["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", -/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), -["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", -hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= -!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p= '0' && b <= '7' ? parseInt(a.substring(1), 8) : b === 'u' || b === 'x' ? parseInt(a.substring(2), 16) : a.charCodeAt(1) } function e (a) { if (a < 32) return (a < 16 ? '\\x0' : '\\x') + a.toString(16); a = String.fromCharCode(a); if (a === '\\' || a === '-' || a === '[' || a === ']')a = '\\' + a; return a } function h (a) { + for (var f = a.substring(1, a.length - 1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g), a = +[], b = [], o = f[0] === '^', c = o ? 1 : 0, i = f.length; c < i; ++c) { var j = f[c]; if (/\\[bdsw]/i.test(j))a.push(j); else { var j = m(j); var d; c + 2 < i && f[c + 1] === '-' ? (d = m(f[c + 2]), c += 2) : d = j; b.push([j, d]); d < 65 || j > 122 || (d < 65 || j > 90 || b.push([Math.max(65, j) | 32, Math.min(d, 90) | 32]), d < 97 || j > 122 || b.push([Math.max(97, j) & -33, Math.min(d, 122) & -33])) } }b.sort(function (a, f) { return a[0] - f[0] || f[1] - a[1] }); f = []; j = [NaN, NaN]; for (c = 0; c < b.length; ++c)i = b[c], i[0] <= j[1] + 1 ? j[1] = Math.max(j[1], i[1]) : f.push(j = i); b = ['[']; o && b.push('^'); b.push.apply(b, a); for (c = 0; c < +f.length; ++c)i = f[c], b.push(e(i[0])), i[1] > i[0] && (i[1] + 1 > i[0] && b.push('-'), b.push(e(i[1]))); b.push(']'); return b.join('') + } function y (a) { + for (var f = a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g), b = f.length, d = [], c = 0, i = 0; c < b; ++c) { var j = f[c]; j === '(' ? ++i : j.charAt(0) === '\\' && (j = +j.substring(1)) && j <= i && (d[j] = -1) } for (c = 1; c < d.length; ++c)d[c] === -1 && (d[c] = ++t); for (i = c = 0; c < b; ++c) { + j = f[c], j === '(' ? (++i, d[i] === void 0 && (f[c] = '(?:')) : j.charAt(0) === '\\' && +(j = +j.substring(1)) && j <= i && (f[c] = '\\' + d[i]) + } for (i = c = 0; c < b; ++c)f[c] === '^' && f[c + 1] !== '^' && (f[c] = ''); if (a.ignoreCase && s) for (c = 0; c < b; ++c)j = f[c], a = j.charAt(0), j.length >= 2 && a === '[' ? f[c] = h(j) : a !== '\\' && (f[c] = j.replace(/[A-Za-z]/g, function (a) { a = a.charCodeAt(0); return '[' + String.fromCharCode(a & -33, a | 32) + ']' })); return f.join('') + } for (var t = 0, s = !1, l = !1, p = 0, d = a.length; p < d; ++p) { var g = a[p]; if (g.ignoreCase)l = !0; else if (/[a-z]/i.test(g.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi, ''))) { s = !0; l = !1; break } } for (var r = +{ b: 8, t: 9, n: 10, v: 11, f: 12, r: 13 }, n = [], p = 0, d = a.length; p < d; ++p) { g = a[p]; if (g.global || g.multiline) throw Error('' + g); n.push('(?:' + y(g) + ')') } return RegExp(n.join('|'), l ? 'gi' : 'g') + } function M (a) { + function m (a) { + switch (a.nodeType) { + case 1:if (e.test(a.className)) break; for (var g = a.firstChild; g; g = g.nextSibling)m(g); g = a.nodeName; if (g === 'BR' || g === 'LI')h[s] = '\n', t[s << 1] = y++, t[s++ << 1 | 1] = a; break; case 3:case 4:g = a.nodeValue, g.length && (g = p ? g.replace(/\r\n?/g, '\n') : g.replace(/[\t\n\r ]+/g, ' '), h[s] = g, t[s << 1] = y, y += g.length, + t[s++ << 1 | 1] = a) + } + } var e = /(?:^|\s)nocode(?:\s|$)/; var h = []; var y = 0; var t = []; var s = 0; let l; a.currentStyle ? l = a.currentStyle.whiteSpace : window.getComputedStyle && (l = document.defaultView.getComputedStyle(a, q).getPropertyValue('white-space')); var p = l && l.substring(0, 3) === 'pre'; m(a); return { a: h.join('').replace(/\n$/, ''), c: t } + } function B (a, m, e, h) { m && (a = { a: m, d: a }, e(a), h.push.apply(h, a.e)) } function x (a, m) { + function e (a) { + for (var l = a.d, p = [l, 'pln'], d = 0, g = a.a.match(y) || [], r = {}, n = 0, z = g.length; n < z; ++n) { + const f = g[n]; let b = r[f]; let o = void 0; var c; if (typeof b === +'string')c = !1; else { var i = h[f.charAt(0)]; if (i)o = f.match(i[1]), b = i[0]; else { for (c = 0; c < t; ++c) if (i = m[c], o = f.match(i[1])) { b = i[0]; break }o || (b = 'pln') } if ((c = b.length >= 5 && b.substring(0, 5) === 'lang-') && !(o && typeof o[1] === 'string'))c = !1, b = 'src'; c || (r[f] = b) }i = d; d += f.length; if (c) { c = o[1]; let j = f.indexOf(c); let k = j + c.length; o[2] && (k = f.length - o[2].length, j = k - c.length); b = b.substring(5); B(l + i, f.substring(0, j), e, p); B(l + i + j, c, C(b, c), p); B(l + i + k, f.substring(k), e, p) } else p.push(l + i, b) + }a.e = p + } var h = {}; let y; (function () { + for (var e = a.concat(m), + l = [], p = {}, d = 0, g = e.length; d < g; ++d) { let r = e[d]; let n = r[3]; if (n) for (let k = n.length; --k >= 0;)h[n.charAt(k)] = r; r = r[1]; n = '' + r; p.hasOwnProperty(n) || (l.push(r), p[n] = q) }l.push(/[\S\s]/); y = L(l) + })(); var t = m.length; return e + } function u (a) { + const m = []; const e = []; a.tripleQuotedStrings ? m.push(['str', /^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/, q, "'\""]) : a.multiLineStrings ? m.push(['str', /^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, + q, "'\"`"]) : m.push(['str', /^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/, q, "\"'"]); a.verbatimStrings && e.push(['str', /^@"(?:[^"]|"")*(?:"|$)/, q]); let h = a.hashComments; h && (a.cStyleComments ? (h > 1 ? m.push(['com', /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, q, '#']) : m.push(['com', /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/, q, '#']), e.push(['str', /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, q])) : m.push(['com', /^#[^\n\r]*/, + q, '#'])); a.cStyleComments && (e.push(['com', /^\/\/[^\n\r]*/, q]), e.push(['com', /^\/\*[\S\s]*?(?:\*\/|$)/, q])); a.regexLiterals && e.push(['lang-regex', /^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]); (h = a.types) && e.push(['typ', h]); a = ('' + a.keywords).replace(/^ | $/g, + ''); a.length && e.push(['kwd', RegExp('^(?:' + a.replace(/[\s,]+/g, '|') + ')\\b'), q]); m.push(['pln', /^\s+/, q, ' \r\n\t\xa0']); e.push(['lit', /^@[$_a-z][\w$@]*/i, q], ['typ', /^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/, q], ['pln', /^[$_a-z][\w$@]*/i, q], ['lit', /^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i, q, '0123456789'], ['pln', /^\\[\S\s]?/, q], ['pun', /^.[^\s\w"-$'./@\\`]*/, q]); return x(m, e) + } function D (a, m) { + function e (a) { + switch (a.nodeType) { + case 1:if (k.test(a.className)) break; if (a.nodeName === 'BR') { + h(a), + a.parentNode && a.parentNode.removeChild(a) + } else for (a = a.firstChild; a; a = a.nextSibling)e(a); break; case 3:case 4:if (p) { let b = a.nodeValue; const d = b.match(t); if (d) { const c = b.substring(0, d.index); a.nodeValue = c; (b = b.substring(d.index + d[0].length)) && a.parentNode.insertBefore(s.createTextNode(b), a.nextSibling); h(a); c || a.parentNode.removeChild(a) } } + } + } function h (a) { + function b (a, d) { const e = d ? a.cloneNode(!1) : a; var f = a.parentNode; if (f) { var f = b(f, 1); let g = a.nextSibling; f.appendChild(e); for (let h = g; h; h = g)g = h.nextSibling, f.appendChild(h) } return e } + for (;!a.nextSibling;) if (a = a.parentNode, !a) return; for (var a = b(a.nextSibling, 0), e; (e = a.parentNode) && e.nodeType === 1;)a = e; d.push(a) + } var k = /(?:^|\s)nocode(?:\s|$)/; var t = /\r\n?|\n/; var s = a.ownerDocument; let l; a.currentStyle ? l = a.currentStyle.whiteSpace : window.getComputedStyle && (l = s.defaultView.getComputedStyle(a, q).getPropertyValue('white-space')); var p = l && l.substring(0, 3) === 'pre'; for (l = s.createElement('LI'); a.firstChild;)l.appendChild(a.firstChild); for (var d = [l], g = 0; g < d.length; ++g)e(d[g]); m === (m | 0) && d[0].setAttribute('value', + m); const r = s.createElement('OL'); r.className = 'linenums'; for (var n = Math.max(0, m - 1 | 0) || 0, g = 0, z = d.length; g < z; ++g)l = d[g], l.className = 'L' + (g + n) % 10, l.firstChild || l.appendChild(s.createTextNode('\xa0')), r.appendChild(l); a.appendChild(r) + } function k (a, m) { for (let e = m.length; --e >= 0;) { const h = m[e]; A.hasOwnProperty(h) ? window.console && console.warn('cannot override language handler %s', h) : A[h] = a } } function C (a, m) { if (!a || !A.hasOwnProperty(a))a = /^\s*= o && (h += 2); e >= c && (a += 2) + } + } catch (w) { 'console' in window && console.log(w && w.stack ? w.stack : w) } + } var v = ['break,continue,do,else,for,if,return,while']; var w = [[v, 'auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile'], + 'catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof']; const F = [w, 'alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where']; const G = [w, 'abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient'] + const H = [G, 'as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var']; var w = [w, 'debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN']; const I = [v, 'and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None'] + const J = [v, 'alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END']; var v = [v, 'case,done,elif,esac,eval,fi,function,in,local,set,then,until']; const K = /^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/; const N = /\S/; const O = u({ + keywords: [F, H, w, 'caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END' + +I, J, v], + hashComments: !0, + cStyleComments: !0, + multiLineStrings: !0, + regexLiterals: !0 + }); var A = {}; k(O, ['default-code']); k(x([], [['pln', /^[^]*(?:>|$)/], ['com', /^<\!--[\S\s]*?(?:--\>|$)/], ['lang-', /^<\?([\S\s]+?)(?:\?>|$)/], ['lang-', /^<%([\S\s]+?)(?:%>|$)/], ['pun', /^(?:<[%?]|[%?]>)/], ['lang-', /^]*>([\S\s]+?)<\/xmp\b[^>]*>/i], ['lang-js', /^]*>([\S\s]*?)(<\/script\b[^>]*>)/i], ['lang-css', /^]*>([\S\s]*?)(<\/style\b[^>]*>)/i], ['lang-in.tag', /^(<\/?[a-z][^<>]*>)/i]]), + ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']); k(x([['pln', /^\s+/, q, ' \t\r\n'], ['atv', /^(?:"[^"]*"?|'[^']*'?)/, q, "\"'"]], [['tag', /^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i], ['atn', /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i], ['lang-uq.val', /^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/], ['pun', /^[/<->]+/], ['lang-js', /^on\w+\s*=\s*"([^"]+)"/i], ['lang-js', /^on\w+\s*=\s*'([^']+)'/i], ['lang-js', /^on\w+\s*=\s*([^\s"'>]+)/i], ['lang-css', /^style\s*=\s*"([^"]+)"/i], ['lang-css', /^style\s*=\s*'([^']+)'/i], ['lang-css', + /^style\s*=\s*([^\s"'>]+)/i]]), ['in.tag']); k(x([], [['atv', /^[\S\s]+/]]), ['uq.val']); k(u({ keywords: F, hashComments: !0, cStyleComments: !0, types: K }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']); k(u({ keywords: 'null,true,false' }), ['json']); k(u({ keywords: H, hashComments: !0, cStyleComments: !0, verbatimStrings: !0, types: K }), ['cs']); k(u({ keywords: G, cStyleComments: !0 }), ['java']); k(u({ keywords: v, hashComments: !0, multiLineStrings: !0 }), ['bsh', 'csh', 'sh']); k(u({ keywords: I, hashComments: !0, multiLineStrings: !0, tripleQuotedStrings: !0 }), + ['cv', 'py']); k(u({ keywords: 'caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END', hashComments: !0, multiLineStrings: !0, regexLiterals: !0 }), ['perl', 'pl', 'pm']); k(u({ keywords: J, hashComments: !0, multiLineStrings: !0, regexLiterals: !0 }), ['rb']); k(u({ keywords: w, cStyleComments: !0, regexLiterals: !0 }), ['js']); k(u({ + keywords: 'all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes', + hashComments: 3, + cStyleComments: !0, + multilineStrings: !0, + tripleQuotedStrings: !0, + regexLiterals: !0 + }), ['coffee']); k(x([], [['str', /^[\S\s]+/]]), ['regex']); window.prettyPrintOne = function (a, m, e) { const h = document.createElement('PRE'); h.innerHTML = a; e && D(h, e); E({ g: m, i: e, h: h }); return h.innerHTML }; window.prettyPrint = function (a) { + function m () { + for (let e = window.PR_SHOULD_USE_CONTINUATION ? l.now() + 250 : Infinity; p < h.length && l.now() < e; p++) { + const n = h[p]; var k = n.className; if (k.indexOf('prettyprint') >= 0) { + var k = k.match(g); var f; var b; if (b = +!k) { b = n; for (var o = void 0, c = b.firstChild; c; c = c.nextSibling) var i = c.nodeType; var o = i === 1 ? o ? b : c : i === 3 ? N.test(c.nodeValue) ? b : o : o; b = (f = o === b ? void 0 : o) && f.tagName === 'CODE' }b && (k = f.className.match(g)); k && (k = k[1]); b = !1; for (o = n.parentNode; o; o = o.parentNode) if ((o.tagName === 'pre' || o.tagName === 'code' || o.tagName === 'xmp') && o.className && o.className.indexOf('prettyprint') >= 0) { b = !0; break }b || ((b = (b = n.className.match(/\blinenums\b(?::(\d+))?/)) ? b[1] && b[1].length ? +b[1] : !0 : !1) && D(n, b), d = { g: k, h: n, i: b }, E(d)) + } + }p < h.length ? setTimeout(m, + 250) : a && a() + } for (var e = [document.getElementsByTagName('pre'), document.getElementsByTagName('code'), document.getElementsByTagName('xmp')], h = [], k = 0; k < e.length; ++k) for (let t = 0, s = e[k].length; t < s; ++t)h.push(e[k][t]); var e = q; var l = Date; l.now || (l = { now: function () { return +new Date() } }); var p = 0; let d; var g = /\blang(?:uage)?-([\w.]+)(?!\S)/; m() + }; window.PR = { + createSimpleLexer: x, + registerLangHandler: k, + sourceDecorator: u, + PR_ATTRIB_NAME: 'atn', + PR_ATTRIB_VALUE: 'atv', + PR_COMMENT: 'com', + PR_DECLARATION: 'dec', + PR_KEYWORD: 'kwd', + PR_LITERAL: 'lit', + PR_NOCODE: 'nocode', + PR_PLAIN: 'pln', + PR_PUNCTUATION: 'pun', + PR_SOURCE: 'src', + PR_STRING: 'str', + PR_TAG: 'tag', + PR_TYPE: 'typ' + } +})() diff --git a/docs/scripts/underscore-min.js b/docs/scripts/underscore-min.js index d22f881..ac1d12f 100644 --- a/docs/scripts/underscore-min.js +++ b/docs/scripts/underscore-min.js @@ -2,5 +2,5 @@ // http://underscorejs.org // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. -(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,g=e.filter,d=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,w=Object.keys,_=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.2";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a=j.keys(n),u=0,i=a.length;i>u;u++)if(t.call(e,n[a[u]],a[u],n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:g&&n.filter===g?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:d&&n.every===d?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={},i=null==r?j.identity:k(r);return A(t,function(r,a){var o=i.call(e,r,a,t);n(u,o,r)}),u}};j.groupBy=F(function(n,t,r){(j.has(n,t)?n[t]:n[t]=[]).push(r)}),j.indexBy=F(function(n,t,r){n[t]=r}),j.countBy=F(function(n,t){j.has(n,t)?n[t]++:n[t]=1}),j.sortedIndex=function(n,t,r,e){r=null==r?j.identity:k(r);for(var u=r.call(e,t),i=0,a=n.length;a>i;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var R=function(){};j.bind=function(n,t){var r,e;if(_&&n.bind===_)return _.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));R.prototype=n.prototype;var u=new R;R.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u,i,a,o;return function(){i=this,u=arguments,a=new Date;var c=function(){var l=new Date-a;t>l?e=setTimeout(c,t-l):(e=null,r||(o=n.apply(i,u)))},l=r&&!e;return e||(e=setTimeout(c,t)),l&&(o=n.apply(i,u)),o}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=w||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},j.pairs=function(n){for(var t=j.keys(n),r=t.length,e=new Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},j.invert=function(n){for(var t={},r=j.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); -//# sourceMappingURL=underscore-min.map \ No newline at end of file +(function () { const n = this; const t = n._; const r = {}; const e = Array.prototype; const u = Object.prototype; const i = Function.prototype; const a = e.push; const o = e.slice; const c = e.concat; const l = u.toString; const f = u.hasOwnProperty; const s = e.forEach; const p = e.map; const h = e.reduce; const v = e.reduceRight; const g = e.filter; const d = e.every; const m = e.some; const y = e.indexOf; const b = e.lastIndexOf; const x = Array.isArray; const w = Object.keys; const _ = i.bind; var j = function (n) { return n instanceof j ? n : this instanceof j ? (this._wrapped = n, void 0) : new j(n) }; typeof exports !== 'undefined' ? (typeof module !== 'undefined' && module.exports && (exports = module.exports = j), exports._ = j) : n._ = j, j.VERSION = '1.5.2'; const A = j.each = j.forEach = function (n, t, e) { if (n != null) if (s && n.forEach === s)n.forEach(t, e); else if (n.length === +n.length) { for (var u = 0, i = n.length; i > u; u++) if (t.call(e, n[u], u, n) === r) return } else for (var a = j.keys(n), u = 0, i = a.length; i > u; u++) if (t.call(e, n[a[u]], a[u], n) === r) return }; j.map = j.collect = function (n, t, r) { const e = []; return n == null ? e : p && n.map === p ? n.map(t, r) : (A(n, function (n, u, i) { e.push(t.call(r, n, u, i)) }), e) }; const E = 'Reduce of empty array with no initial value'; j.reduce = j.foldl = j.inject = function (n, t, r, e) { let u = arguments.length > 2; if (n == null && (n = []), h && n.reduce === h) return e && (t = j.bind(t, e)), u ? n.reduce(t, r) : n.reduce(t); if (A(n, function (n, i, a) { u ? r = t.call(e, r, n, i, a) : (r = n, u = !0) }), !u) throw new TypeError(E); return r }, j.reduceRight = j.foldr = function (n, t, r, e) { let u = arguments.length > 2; if (n == null && (n = []), v && n.reduceRight === v) return e && (t = j.bind(t, e)), u ? n.reduceRight(t, r) : n.reduceRight(t); let i = n.length; if (i !== +i) { var a = j.keys(n); i = a.length } if (A(n, function (o, c, l) { c = a ? a[--i] : --i, u ? r = t.call(e, r, n[c], c, l) : (r = n[c], u = !0) }), !u) throw new TypeError(E); return r }, j.find = j.detect = function (n, t, r) { let e; return O(n, function (n, u, i) { return t.call(r, n, u, i) ? (e = n, !0) : void 0 }), e }, j.filter = j.select = function (n, t, r) { const e = []; return n == null ? e : g && n.filter === g ? n.filter(t, r) : (A(n, function (n, u, i) { t.call(r, n, u, i) && e.push(n) }), e) }, j.reject = function (n, t, r) { return j.filter(n, function (n, e, u) { return !t.call(r, n, e, u) }, r) }, j.every = j.all = function (n, t, e) { t || (t = j.identity); let u = !0; return n == null ? u : d && n.every === d ? n.every(t, e) : (A(n, function (n, i, a) { return (u = u && t.call(e, n, i, a)) ? void 0 : r }), !!u) }; var O = j.some = j.any = function (n, t, e) { t || (t = j.identity); let u = !1; return n == null ? u : m && n.some === m ? n.some(t, e) : (A(n, function (n, i, a) { return u || (u = t.call(e, n, i, a)) ? r : void 0 }), !!u) }; j.contains = j.include = function (n, t) { return n == null ? !1 : y && n.indexOf === y ? n.indexOf(t) != -1 : O(n, function (n) { return n === t }) }, j.invoke = function (n, t) { const r = o.call(arguments, 2); const e = j.isFunction(t); return j.map(n, function (n) { return (e ? t : n[t]).apply(n, r) }) }, j.pluck = function (n, t) { return j.map(n, function (n) { return n[t] }) }, j.where = function (n, t, r) { return j.isEmpty(t) ? r ? void 0 : [] : j[r ? 'find' : 'filter'](n, function (n) { for (const r in t) if (t[r] !== n[r]) return !1; return !0 }) }, j.findWhere = function (n, t) { return j.where(n, t, !0) }, j.max = function (n, t, r) { if (!t && j.isArray(n) && n[0] === +n[0] && n.length < 65535) return Math.max.apply(Math, n); if (!t && j.isEmpty(n)) return -1 / 0; let e = { computed: -1 / 0, value: -1 / 0 }; return A(n, function (n, u, i) { const a = t ? t.call(r, n, u, i) : n; a > e.computed && (e = { value: n, computed: a }) }), e.value }, j.min = function (n, t, r) { if (!t && j.isArray(n) && n[0] === +n[0] && n.length < 65535) return Math.min.apply(Math, n); if (!t && j.isEmpty(n)) return 1 / 0; let e = { computed: 1 / 0, value: 1 / 0 }; return A(n, function (n, u, i) { const a = t ? t.call(r, n, u, i) : n; a < e.computed && (e = { value: n, computed: a }) }), e.value }, j.shuffle = function (n) { let t; let r = 0; const e = []; return A(n, function (n) { t = j.random(r++), e[r - 1] = e[t], e[t] = n }), e }, j.sample = function (n, t, r) { return arguments.length < 2 || r ? n[j.random(n.length - 1)] : j.shuffle(n).slice(0, Math.max(0, t)) }; const k = function (n) { return j.isFunction(n) ? n : function (t) { return t[n] } }; j.sortBy = function (n, t, r) { const e = k(t); return j.pluck(j.map(n, function (n, t, u) { return { value: n, index: t, criteria: e.call(r, n, t, u) } }).sort(function (n, t) { const r = n.criteria; const e = t.criteria; if (r !== e) { if (r > e || r === void 0) return 1; if (e > r || e === void 0) return -1 } return n.index - t.index }), 'value') }; const F = function (n) { return function (t, r, e) { const u = {}; const i = r == null ? j.identity : k(r); return A(t, function (r, a) { const o = i.call(e, r, a, t); n(u, o, r) }), u } }; j.groupBy = F(function (n, t, r) { (j.has(n, t) ? n[t] : n[t] = []).push(r) }), j.indexBy = F(function (n, t, r) { n[t] = r }), j.countBy = F(function (n, t) { j.has(n, t) ? n[t]++ : n[t] = 1 }), j.sortedIndex = function (n, t, r, e) { r = r == null ? j.identity : k(r); for (var u = r.call(e, t), i = 0, a = n.length; a > i;) { const o = i + a >>> 1; r.call(e, n[o]) < u ? i = o + 1 : a = o } return i }, j.toArray = function (n) { return n ? j.isArray(n) ? o.call(n) : n.length === +n.length ? j.map(n, j.identity) : j.values(n) : [] }, j.size = function (n) { return n == null ? 0 : n.length === +n.length ? n.length : j.keys(n).length }, j.first = j.head = j.take = function (n, t, r) { return n == null ? void 0 : t == null || r ? n[0] : o.call(n, 0, t) }, j.initial = function (n, t, r) { return o.call(n, 0, n.length - (t == null || r ? 1 : t)) }, j.last = function (n, t, r) { return n == null ? void 0 : t == null || r ? n[n.length - 1] : o.call(n, Math.max(n.length - t, 0)) }, j.rest = j.tail = j.drop = function (n, t, r) { return o.call(n, t == null || r ? 1 : t) }, j.compact = function (n) { return j.filter(n, j.identity) }; var M = function (n, t, r) { return t && j.every(n, j.isArray) ? c.apply(r, n) : (A(n, function (n) { j.isArray(n) || j.isArguments(n) ? t ? a.apply(r, n) : M(n, t, r) : r.push(n) }), r) }; j.flatten = function (n, t) { return M(n, t, []) }, j.without = function (n) { return j.difference(n, o.call(arguments, 1)) }, j.uniq = j.unique = function (n, t, r, e) { j.isFunction(t) && (e = r, r = t, t = !1); const u = r ? j.map(n, r, e) : n; const i = []; const a = []; return A(u, function (r, e) { (t ? e && a[a.length - 1] === r : j.contains(a, r)) || (a.push(r), i.push(n[e])) }), i }, j.union = function () { return j.uniq(j.flatten(arguments, !0)) }, j.intersection = function (n) { const t = o.call(arguments, 1); return j.filter(j.uniq(n), function (n) { return j.every(t, function (t) { return j.indexOf(t, n) >= 0 }) }) }, j.difference = function (n) { const t = c.apply(e, o.call(arguments, 1)); return j.filter(n, function (n) { return !j.contains(t, n) }) }, j.zip = function () { for (var n = j.max(j.pluck(arguments, 'length').concat(0)), t = new Array(n), r = 0; n > r; r++)t[r] = j.pluck(arguments, '' + r); return t }, j.object = function (n, t) { if (n == null) return {}; for (var r = {}, e = 0, u = n.length; u > e; e++)t ? r[n[e]] = t[e] : r[n[e][0]] = n[e][1]; return r }, j.indexOf = function (n, t, r) { if (n == null) return -1; let e = 0; const u = n.length; if (r) { if (typeof r !== 'number') return e = j.sortedIndex(n, t), n[e] === t ? e : -1; e = r < 0 ? Math.max(0, u + r) : r } if (y && n.indexOf === y) return n.indexOf(t, r); for (;u > e; e++) if (n[e] === t) return e; return -1 }, j.lastIndexOf = function (n, t, r) { if (n == null) return -1; const e = r != null; if (b && n.lastIndexOf === b) return e ? n.lastIndexOf(t, r) : n.lastIndexOf(t); for (let u = e ? r : n.length; u--;) if (n[u] === t) return u; return -1 }, j.range = function (n, t, r) { arguments.length <= 1 && (t = n || 0, n = 0), r = arguments[2] || 1; for (var e = Math.max(Math.ceil((t - n) / r), 0), u = 0, i = new Array(e); e > u;)i[u++] = n, n += r; return i }; const R = function () {}; j.bind = function (n, t) { let r, e; if (_ && n.bind === _) return _.apply(n, o.call(arguments, 1)); if (!j.isFunction(n)) throw new TypeError(); return r = o.call(arguments, 2), e = function () { if (!(this instanceof e)) return n.apply(t, r.concat(o.call(arguments))); R.prototype = n.prototype; const u = new R(); R.prototype = null; const i = n.apply(u, r.concat(o.call(arguments))); return Object(i) === i ? i : u } }, j.partial = function (n) { const t = o.call(arguments, 1); return function () { return n.apply(this, t.concat(o.call(arguments))) } }, j.bindAll = function (n) { const t = o.call(arguments, 1); if (t.length === 0) throw new Error('bindAll must be passed function names'); return A(t, function (t) { n[t] = j.bind(n[t], n) }), n }, j.memoize = function (n, t) { const r = {}; return t || (t = j.identity), function () { const e = t.apply(this, arguments); return j.has(r, e) ? r[e] : r[e] = n.apply(this, arguments) } }, j.delay = function (n, t) { const r = o.call(arguments, 2); return setTimeout(function () { return n.apply(null, r) }, t) }, j.defer = function (n) { return j.delay.apply(j, [n, 1].concat(o.call(arguments, 1))) }, j.throttle = function (n, t, r) { let e; let u; let i; let a = null; let o = 0; r || (r = {}); const c = function () { o = r.leading === !1 ? 0 : new Date(), a = null, i = n.apply(e, u) }; return function () { const l = new Date(); o || r.leading !== !1 || (o = l); const f = t - (l - o); return e = this, u = arguments, f <= 0 ? (clearTimeout(a), a = null, o = l, i = n.apply(e, u)) : a || r.trailing === !1 || (a = setTimeout(c, f)), i } }, j.debounce = function (n, t, r) { let e, u, i, a, o; return function () { i = this, u = arguments, a = new Date(); var c = function () { const l = new Date() - a; t > l ? e = setTimeout(c, t - l) : (e = null, r || (o = n.apply(i, u))) }; const l = r && !e; return e || (e = setTimeout(c, t)), l && (o = n.apply(i, u)), o } }, j.once = function (n) { let t; let r = !1; return function () { return r ? t : (r = !0, t = n.apply(this, arguments), n = null, t) } }, j.wrap = function (n, t) { return function () { const r = [n]; return a.apply(r, arguments), t.apply(this, r) } }, j.compose = function () { const n = arguments; return function () { for (var t = arguments, r = n.length - 1; r >= 0; r--)t = [n[r].apply(this, t)]; return t[0] } }, j.after = function (n, t) { return function () { return --n < 1 ? t.apply(this, arguments) : void 0 } }, j.keys = w || function (n) { if (n !== Object(n)) throw new TypeError('Invalid object'); const t = []; for (const r in n)j.has(n, r) && t.push(r); return t }, j.values = function (n) { for (var t = j.keys(n), r = t.length, e = new Array(r), u = 0; r > u; u++)e[u] = n[t[u]]; return e }, j.pairs = function (n) { for (var t = j.keys(n), r = t.length, e = new Array(r), u = 0; r > u; u++)e[u] = [t[u], n[t[u]]]; return e }, j.invert = function (n) { for (var t = {}, r = j.keys(n), e = 0, u = r.length; u > e; e++)t[n[r[e]]] = r[e]; return t }, j.functions = j.methods = function (n) { const t = []; for (const r in n)j.isFunction(n[r]) && t.push(r); return t.sort() }, j.extend = function (n) { return A(o.call(arguments, 1), function (t) { if (t) for (const r in t)n[r] = t[r] }), n }, j.pick = function (n) { const t = {}; const r = c.apply(e, o.call(arguments, 1)); return A(r, function (r) { r in n && (t[r] = n[r]) }), t }, j.omit = function (n) { const t = {}; const r = c.apply(e, o.call(arguments, 1)); for (const u in n)j.contains(r, u) || (t[u] = n[u]); return t }, j.defaults = function (n) { return A(o.call(arguments, 1), function (t) { if (t) for (const r in t)n[r] === void 0 && (n[r] = t[r]) }), n }, j.clone = function (n) { return j.isObject(n) ? j.isArray(n) ? n.slice() : j.extend({}, n) : n }, j.tap = function (n, t) { return t(n), n }; var S = function (n, t, r, e) { if (n === t) return n !== 0 || 1 / n == 1 / t; if (n == null || t == null) return n === t; n instanceof j && (n = n._wrapped), t instanceof j && (t = t._wrapped); const u = l.call(n); if (u != l.call(t)) return !1; switch (u) { case '[object String]':return n == String(t); case '[object Number]':return n != +n ? t != +t : n == 0 ? 1 / n == 1 / t : n == +t; case '[object Date]':case '[object Boolean]':return +n == +t; case '[object RegExp]':return n.source == t.source && n.global == t.global && n.multiline == t.multiline && n.ignoreCase == t.ignoreCase } if (typeof n !== 'object' || typeof t !== 'object') return !1; for (let i = r.length; i--;) if (r[i] == n) return e[i] == t; const a = n.constructor; const o = t.constructor; if (a !== o && !(j.isFunction(a) && a instanceof a && j.isFunction(o) && o instanceof o)) return !1; r.push(n), e.push(t); let c = 0; let f = !0; if (u == '[object Array]') { if (c = n.length, f = c == t.length) for (;c-- && (f = S(n[c], t[c], r, e));); } else { for (var s in n) if (j.has(n, s) && (c++, !(f = j.has(t, s) && S(n[s], t[s], r, e)))) break; if (f) { for (s in t) if (j.has(t, s) && !c--) break; f = !c } } return r.pop(), e.pop(), f }; j.isEqual = function (n, t) { return S(n, t, [], []) }, j.isEmpty = function (n) { if (n == null) return !0; if (j.isArray(n) || j.isString(n)) return n.length === 0; for (const t in n) if (j.has(n, t)) return !1; return !0 }, j.isElement = function (n) { return !(!n || n.nodeType !== 1) }, j.isArray = x || function (n) { return l.call(n) == '[object Array]' }, j.isObject = function (n) { return n === Object(n) }, A(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function (n) { j['is' + n] = function (t) { return l.call(t) == '[object ' + n + ']' } }), j.isArguments(arguments) || (j.isArguments = function (n) { return !(!n || !j.has(n, 'callee')) }), typeof /./ !== 'function' && (j.isFunction = function (n) { return typeof n === 'function' }), j.isFinite = function (n) { return isFinite(n) && !isNaN(parseFloat(n)) }, j.isNaN = function (n) { return j.isNumber(n) && n != +n }, j.isBoolean = function (n) { return n === !0 || n === !1 || l.call(n) == '[object Boolean]' }, j.isNull = function (n) { return n === null }, j.isUndefined = function (n) { return n === void 0 }, j.has = function (n, t) { return f.call(n, t) }, j.noConflict = function () { return n._ = t, this }, j.identity = function (n) { return n }, j.times = function (n, t, r) { for (var e = Array(Math.max(0, n)), u = 0; n > u; u++)e[u] = t.call(r, u); return e }, j.random = function (n, t) { return t == null && (t = n, n = 0), n + Math.floor(Math.random() * (t - n + 1)) }; const I = { escape: { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } }; I.unescape = j.invert(I.escape); const T = { escape: new RegExp('[' + j.keys(I.escape).join('') + ']', 'g'), unescape: new RegExp('(' + j.keys(I.unescape).join('|') + ')', 'g') }; j.each(['escape', 'unescape'], function (n) { j[n] = function (t) { return t == null ? '' : ('' + t).replace(T[n], function (t) { return I[n][t] }) } }), j.result = function (n, t) { if (n == null) return void 0; const r = n[t]; return j.isFunction(r) ? r.call(n) : r }, j.mixin = function (n) { A(j.functions(n), function (t) { const r = j[t] = n[t]; j.prototype[t] = function () { const n = [this._wrapped]; return a.apply(n, arguments), z.call(this, r.apply(j, n)) } }) }; let N = 0; j.uniqueId = function (n) { const t = ++N + ''; return n ? n + t : t }, j.templateSettings = { evaluate: /<%([\s\S]+?)%>/g, interpolate: /<%=([\s\S]+?)%>/g, escape: /<%-([\s\S]+?)%>/g }; const q = /(.)^/; const B = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', ' ': 't', '\u2028': 'u2028', '\u2029': 'u2029' }; const D = /\\|'|\r|\n|\t|\u2028|\u2029/g; j.template = function (n, t, r) { let e; r = j.defaults({}, r, j.templateSettings); const u = new RegExp([(r.escape || q).source, (r.interpolate || q).source, (r.evaluate || q).source].join('|') + '|$', 'g'); let i = 0; let a = "__p+='"; n.replace(u, function (t, r, e, u, o) { return a += n.slice(i, o).replace(D, function (n) { return '\\' + B[n] }), r && (a += "'+\n((__t=(" + r + "))==null?'':_.escape(__t))+\n'"), e && (a += "'+\n((__t=(" + e + "))==null?'':__t)+\n'"), u && (a += "';\n" + u + "\n__p+='"), i = o + t.length, t }), a += "';\n", r.variable || (a = 'with(obj||{}){\n' + a + '}\n'), a = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + a + 'return __p;\n'; try { e = new Function(r.variable || 'obj', '_', a) } catch (o) { throw o.source = a, o } if (t) return e(t, j); const c = function (n) { return e.call(this, n, j) }; return c.source = 'function(' + (r.variable || 'obj') + '){\n' + a + '}', c }, j.chain = function (n) { return j(n).chain() }; var z = function (n) { return this._chain ? j(n).chain() : n }; j.mixin(j), A(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function (n) { const t = e[n]; j.prototype[n] = function () { const r = this._wrapped; return t.apply(r, arguments), n != 'shift' && n != 'splice' || r.length !== 0 || delete r[0], z.call(this, r) } }), A(['concat', 'join', 'slice'], function (n) { const t = e[n]; j.prototype[n] = function () { return z.call(this, t.apply(this._wrapped, arguments)) } }), j.extend(j.prototype, { chain: function () { return this._chain = !0, this }, value: function () { return this._wrapped } }) }).call(this) +// # sourceMappingURL=underscore-min.map diff --git a/lib/bucket.js b/lib/bucket.js deleted file mode 100644 index 05c0746..0000000 --- a/lib/bucket.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' - -const matflap = require('matflap') - -/** - * Creates a new Bucket instance - * - * @constructor - * @param {String} [hash=null] - The hash value of the Bucket. - * @param {Array|Object} [elements=null] - Any element(s) to add to the bucket. - * - * @returns {Bucket} - */ -class Bucket { - constructor (hash = null, elements = null) { - this.hash = hash - - this._ = [] - this._size = 0 - if (elements) this.add(elements) - } - - add (elements) { - if (!elements) return - - const isArray = Array.isArray(elements) - - if (isArray) { - const flatMap = matflap(elements, (element) => [ - element._id.toHexString(), - JSON.stringify(element) - ]) - this._.push(...flatMap) - this._size += elements.length - } - - if (elements.constructor.name === 'Object' && !isArray) { - this._.push(elements._id.toHexString(), JSON.stringify(elements)) - this._size++ - } - } -} - -function makeBucket (hash, elements) { - return new Bucket(hash, elements) -} - -/* - 2^24 / 2^9 = 2^15 possible buckets - - https://redis.io/topics/memory-optimization - - Note: MongoDB ObjectID counter value (last 3 bytes) may not be unique in rare circumstances. - This may degrade look-up performance slightly, but will not change the behavior of bucketing. - */ -function hashFunction (documentID) { - const hexString = - documentID.constructor.name === 'String' - ? documentID - : documentID.toHexString() - - const counter = parseInt(hexString.slice(18), 16) - const quotient = Math.floor(counter / 512) - - return quotient.toString() -} - -module.exports = { Bucket, makeBucket, hashFunction } diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index 14cc863..0000000 --- a/lib/cache.js +++ /dev/null @@ -1,592 +0,0 @@ -'use strict' - -const pump = require('pump') -const sliced = require('sliced') -const matflap = require('matflap') -const { PassThrough } = require('stream') -const { createClient } = require('redis') -const { promisify } = require('util') - -const { makeBucket, hashFunction } = require('./bucket') -const { - SYMBOLS: { NULL_DOC, EMPTY_QUERY }, - validatePaginationOpts, - filterAvailableWideMatch, - fastReverse, - hydrate -} = require('./utils') -const { FerretError } = require('./error') - -const debug = require('util').debuglog('fireferret::cache') - -/** - * Creates a new Cache instance. - * @constructor - * @param {object} options - Cache options - * @param {redisOptions} redisOptions - Redis client options. - * @returns {Cache} - The FireFerret Cache controller. - */ -class Cache { - constructor (options, redisOptions) { - this.options = options - this.redisOptions = redisOptions - this.redis = null - } - - /** - * Internal - Using this may induce unintended behaviors. - * @param {RedisClient} client - An npm::redis client that has already been connected. - */ - _setClient (client) { - if (client) this.redis = client - } - - /** - * Internal - Using this may induce unintended behaviors. - * @returns An npm::redis client. - */ - _getClient () { - return this.redis - } - - /** - * Attempts to establish a connection to Redis - * @returns {Promise} Resolves - */ - async connect () { - if (this.redis) return - - this.redis = createClient(this.redisOptions) - - this.redis.on('ready', () => { - debug('redis server info', this.redis.server_info) - }) - - return new Promise((resolve, reject) => { - this.redis.on('connect', () => { - clearTimeout(timeout) - - debug('redis client connected') - - resolve('ok') - }) - - const timeout = setTimeout(() => { - reject( - new FerretError( - 'ConnectionError', - 'failed to connected before timeout', - 'redis::connect', - { timeout: this.redisOptions.connectionTimeout } - ) - ) - }, this.redisOptions.connectionTimeout) - }) - } - - /** - * Attempts to close the Redis client collection. - * @returns {void} - */ - async close () { - const _quit = promisify(this.redis.quit).bind(this.redis) - - try { - const reply = await _quit() - - debug('redis client closed successfully') - - return reply - } catch (err) { - throw new FerretError( - 'ConnectionError', - 'close operation has failed -- quit must be invoked', - 'redis::close', - err - ) - } - } - - /** - * @typedef {Array} QueryList - An Array of id values associated with a particular query. - */ - /** - * Retrieves cached documents from a queryList. - * - * @param {Array} queryList - The list of document IDs. - * @param {Object} [options={}] - * @param {boolean} [options.hydrate=false] - JSON.parse documents and attempt to reformat types (performance hit). - * @param {boolean} [options.stream=false] - Return as a stream. - * @param {boolean} [options.ndjson=false] - When streaming, use the ndJSON spec. - * - * @returns {Array|Stream} - */ - async getDocuments (queryList, queryKey, options) { - if (!options) options = {} - - const cacheOps = this.debucketify(queryList, queryKey.collectionName) - - if (!options.stream) { - debug(`looking up ${queryList.length} documents`) - - const rawDocuments = await this.multihmget(cacheOps) - - const map = options.hydrate ? hydrate : JSON.parse - return matflap(rawDocuments, map) - } - - const source = await this.multihmget(cacheOps, true) - const dest = PassThrough() - - pump(source, dest) - - return dest - } - - /** - * Sets the queryList and caches documents into Redis. - * @param {QueryKey} queryKey - The QueryKey to use when caching. - * @param {Array} documents - The documents to cache (from MongoDB). - * - * @returns {void} - */ - async setDocuments (queryKey, documents) { - if (documents.length === 0) { - debug('caching EMPTY_QUERY into QueryList') - - this.lpush(queryKey.toString(), [EMPTY_QUERY.description]) - - return - } - - const { buckets, idCapture } = this.bucketify(documents) - - const hashes = Object.keys(buckets) - - debug(`caching ${documents.length} documents in ${hashes.length} buckets`) - - for (let i = 0; i < hashes.length; i++) { - this.hmset( - `${queryKey.collectionName}:${hashes[i]}`, - buckets[hashes[i]]._ - ) - } - - this.batchlpush(queryKey.toString(), idCapture) - } - - /** - * Retrieve a single document from a bucket. - * - * @param {String} documentID - The document ID as a string. - * - * @returns {String} The document data. - */ - async getDocument (documentID, collectionName) { - const hash = `${collectionName}:${hashFunction(documentID)}` - - return this.hget(hash, documentID) - } - - /** - * Caches a single document into an appropriate bucket. - * - * @param {Object} document - The document to cache. - * @param {String|ObjectID} requestedID - The document ID as a ObjectID instance or String. - * - * @returns {void} - */ - async setDocument (document, requestedID, collectionName) { - const documentID = - document && document._id ? document._id.toHexString() : requestedID - const hash = `${collectionName}:${hashFunction(documentID)}` - - if (document) { - document = JSON.stringify(document) - } else { - document = NULL_DOC.description - } - - const args = [documentID, document] - - debug(`Setting document into bucket: ${hash}`) - this.hset(hash, args) - } - - /** - * @typedef {Object} QueryMatch - * - * @property {Array} queryList - The list of document IDs. - * @property {String} matchType - The type of match strategy used. - */ - /** - * Gets the queryList associated with the provided QueryKey - * @param {QueryKey} queryKey - The QueryKey - * @returns {QueryMatch} - */ - async getQueryList (queryKey) { - let matchType = null - let queryList = await this.lrange(queryKey.toString()) - - if (!queryList || queryList.length === 0) { - if (this.options.wideMatch) { - queryList = await this.getWideMatch(queryKey) - matchType = 'wide' - } else { - queryList = null - } - } - - return { queryList, matchType } - } - - /** - * Sets the queryList - * - * @param {QueryKey} queryKey - The QueryKey. - * @param {Array} queryList - The list of document IDs. - * - * @returns {void} - */ - async setQueryList (queryKey, queryList) { - this.batchlpush(queryKey.toString(), queryList) - } - - /** - * Gets the document ID associated with a particular query - * - * @param {String} queryHash - The hash name version of a query. - * @param {String} query - The query to retreive. - * - * @returns {String} The document ID. - */ - async getQueryHash (queryHash, query) { - return this.hget(queryHash, query) - } - - /** - * Sets a key-value pair in the queryHash - * - * @param {String} queryHash - The hash name version of a query. - * @param {...any} args - The query-documentID pair to set. - * - * @returns {void} - */ - async setQueryHash (queryHash, ...args) { - // [queryHash, query] = args - this.hset(queryHash, args) - } - - /** - * Attempts to find a previously cached query that contains our requested data. - * - * @param {QueryKey} queryKey - The QueryKey. - * - * @returns {Array|null} - */ - async getWideMatch (queryKey) { - const scanPattern = `${queryKey.baseKey()}*` - - const [, cachedQueries] = await this.scan(0, scanPattern) - - if (cachedQueries && cachedQueries.length > 0) { - const pageOptions = validatePaginationOpts(queryKey.queryOptions) || {} - - const { targetQuery, rangeOptions } = - filterAvailableWideMatch(cachedQueries, pageOptions) || {} - - if (targetQuery && rangeOptions) { - debug('Valid Wide-Match was found!') - return this.lrange( - targetQuery, - rangeOptions.start, - rangeOptions.end - 1 - ) - } - } - - debug('No valid Wide-Matches were found.') - return null - } - - /** - * Determine which buckets need to be cached. - * - * @param {Array} documents - Array of MongoDB documents to cache. - * - * @returns {Object} - */ - bucketify (documents) { - const buckets = {} - const idCapture = [] - - /* generate our cache operations */ - for (let i = 0; i < documents.length; i++) { - const hexString = documents[i]._id.toHexString() - - idCapture.push(hexString) - - const hash = hashFunction(hexString) - const existingBucket = buckets[hash] - - if (existingBucket) existingBucket.add(documents[i]) - else buckets[hash] = makeBucket(hash, documents[i]) - } - - return { buckets, idCapture } - } - - /** - * Determine which buckets need to be retrieved. - * - * @param {Array} queryList - The list of document IDs. - * - * @returns {Object} The cache operations to carry out. - */ - debucketify (queryList, collectionName) { - if (!queryList || queryList.length === 0) return null - - const cacheOps = {} - for (let i = 0; i < queryList.length; i++) { - const documentID = queryList[i] - const hash = `${collectionName}:${hashFunction(documentID)}` - - const existingHash = cacheOps[hash] - - if (existingHash) existingHash.push(documentID) - else cacheOps[hash] = [documentID] - } - - return cacheOps - } - - /* Single operation wrappers */ - async hget (hash, field) { - const _hget = promisify(this.redis.hget).bind(this.redis) - - try { - return _hget(hash, field) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hget operation has failed', - 'redis::hget', - err - ) - } - } - - async hmget (key, fields) { - const _hmget = promisify(this.redis.hmget).bind(this.redis) - - try { - return _hmget(key, fields) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hmget operation has failed', - 'redis::hmget', - err - ) - } - } - - async hset (hash, args) { - const _hset = promisify(this.redis.hset).bind(this.redis) - - try { - await _hset(hash, args) - } catch (err) { - debug(err) - throw new FerretError( - 'RedisError', - 'hset operation has failed', - 'redis::hset', - err - ) - } - } - - async hmset (key, args) { - const _hmset = promisify(this.redis.hmset).bind(this.redis) - - try { - await _hmset(key, args) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hmset operation has failed', - 'redis::hmset', - err - ) - } - } - - async lrange (key, start, end) { - const _lrange = promisify(this.redis.lrange).bind(this.redis) - - try { - if (!isNaN(start) || !isNaN(end)) { - start = start || start === 0 ? start : 0 - end = end || end === 0 ? end : -1 - - return _lrange(key, start, end) - } - - return _lrange(key, 0, -1) - } catch (err) { - throw new FerretError( - 'RedisError', - 'lrange operation has failed', - 'redis::lrange', - err - ) - } - } - - async lpush (key, elements, reverse = true) { - if (!key || key.length === 0) { - throw new FerretError( - 'InvalidArguments', - 'valid key is required for lpush', - 'redis::lpush', - { key } - ) - } - - if (!elements || elements.length === 0) { - throw new FerretError( - 'InvalidArguments', - 'Elements are required for lpush', - 'redis::lpush', - { elements } - ) - } - - const _lpush = promisify(this.redis.lpush).bind(this.redis) - if (reverse) fastReverse(elements) - - try { - const reply = await _lpush(key, ...elements) - - return reply - } catch (err) { - throw new FerretError( - 'RedisError', - 'lpush operation has failed', - 'redis::lpush', - err - ) - } - } - - async scan (cursor, pattern, count = this.redisOptions.count) { - if ((!cursor && cursor !== 0) || !pattern) { - throw new FerretError( - 'RedisError', - 'cursor and pattern are required parameters for a SCAN operation', - 'redis::scan', - { cursor, pattern } - ) - } - - const _scan = promisify(this.redis.scan).bind(this.redis) - - try { - const elements = _scan(cursor, 'MATCH', pattern, 'COUNT', count) - - return elements - } catch (err) { - throw new FerretError( - 'RedisError', - 'scan operation has failed', - 'redis::scan', - err - ) - } - } - - /* Async multi operations */ - async multihmget (operations, stream) { - const hashes = Object.keys(operations) - - if (!stream) { - const multi = this.redis.multi() - - for (let i = 0; i < hashes.length; i++) { - multi.hmget(hashes[i], operations[hashes[i]]) - } - - const exec = promisify(multi.exec).bind(multi) - - try { - const collation = await exec() - - return collation - } catch (err) { - throw new FerretError( - 'RedisError', - 'one or more hgetall operations have failed', - 'redis::multihmget', - err - ) - } - } - - /* using streams */ - const source = PassThrough() - const sink = PassThrough() - - let fresh = true - for ( - let i = 0, hash = hashes[i]; - i < hashes.length; - i++, hash = hashes[i] - ) { - this.hmget(hash, operations[hash]).then((bucket) => { - source.write( - `${ - (fresh && !this.redisOptions.ndJSON ? '[' : '') + - bucket.join(!this.redisOptions.ndJSON ? ',' : '\n') - }` - ) - fresh = false - - /* if this is the last bucket, end the stream appropriately */ - if (this.redisOptions.ndJSON && i === hashes.length - 1) source.end() - if (!this.redisOptions.ndJSON && i === hashes.length - 1) { - source.end(']') - } else if (!this.redisOptions.ndJSON) { - /* 'join' the batches together (array return) */ - source.write(',') - } - }) - } - - pump(source, sink) - - return sink - } - - /* Batch operations wrap single operation */ - async batchlpush (key, elements, batchSize = this.redisOptions.batchSize) { - if (batchSize > elements.length) return this.lpush(key, elements, true) - - const elementCount = elements.length - const batchOps = [] - - /* retain document parity with mongo */ - fastReverse(elements) - - for (let i = 0; i < Math.ceil(elementCount / batchSize); i += 1) { - const end = - elementCount < (i + 1) * batchSize ? elementCount : (i + 1) * batchSize - const batch = sliced(elements, i * batchSize, end) - if (batch.length > 0) batchOps.push(this.lpush(key, batch, false)) - } - - return Promise.all(batchOps) - } -} - -module.exports = Cache diff --git a/lib/client.js b/lib/client.js deleted file mode 100644 index 679492d..0000000 --- a/lib/client.js +++ /dev/null @@ -1,289 +0,0 @@ -'use strict' - -const { ObjectID } = require('mongodb') - -const MongoClient = require('./mongo') -const Cache = require('./cache') -const { generateOptions } = require('./options') -const { - SYMBOLS: { EMPTY_QUERY, CACHE_MISS, CACHE_HIT, NULL_DOC }, - validatePaginationOpts, - hydrate, - fastReverse -} = require('./utils') -const { FerretError } = require('./error') -const { QueryKey } = require('./key') - -const debug = require('util').debuglog('fireferret::main') - -/** - * Creates a new FireFerret instance - * @constructor - * @param {options.FireFerretOptions} options - FireFerret configuration options - * @returns {FireFerret} - */ -class FireFerret { - constructor (options) { - const mongoOpts = generateOptions(options.mongo, 'mongo') - const cacheOpts = generateOptions(options, 'cache') - - const mongoClientOpts = generateOptions(options.mongo, 'mongoClient') - const redisClientOpts = generateOptions(options.redis, 'redisClient') - - this.mongo = new MongoClient(mongoOpts.uri, mongoOpts, mongoClientOpts) - this.cache = new Cache(cacheOpts, redisClientOpts) - - debug('FireFerret client created') - } - - /** - * Establish a connection to both data stores. - * @returns {string} The connection status. - */ - async connect () { - await this.cache.connect() - await this.mongo.connect() - - return 'ok' - } - - /** - * Gracefully close all active connections. - * @returns {string} The close status. - */ - async close () { - await this.cache.close() - await this.mongo.close() - - return 'ok' - } - - /** - * @typedef {Object} Query - * You can find more information on querying documents {@link https://docs.mongodb.com/manual/tutorial/query-documents/|here}. - * - * @example - * { name: { $in: ['Foo', 'Bar'] } } - */ - /** - * Fetch MongoDB documents from a query. - * - * @param {Query} [query={}] - An optional cursor query object. - * @param {Object} [options={}] - Optional settings. - * @param {String} [collection=null] - The collection to fetch from when not using the default. - * @param {Boolean} [options.hydrate=false] - JSON.parse documents and attempt to reformat types (performance hit). - * @param {Boolean} [options.stream=false] - Return the documents as a stream. - * @param {Boolean} [options.wideMatch=false] - Use Wide-Match strategy. - * - * @returns {Array|null} documents - */ - async fetch (query, options, collectionName = null) { - if (!options) options = {} - if (!query) query = {} - - const queryKey = new QueryKey( - this.mongo.dbName, - collectionName || this.mongo.collectionName, - query, - options - ) - - const { queryList, matchType } = await this.cache.getQueryList(queryKey) - - const verdict = - !queryList || queryList.length === 0 - ? CACHE_MISS - : queryList && queryList[0] === EMPTY_QUERY.description - ? EMPTY_QUERY - : CACHE_HIT - debug(verdict.description, '@', queryKey.toString()) - - if (CACHE_HIT === verdict) { - /* check match type and cache new QL if needed */ - if (matchType === 'wide') { - this.cache.setQueryList(queryKey, queryList) - return this.cache.getDocuments(fastReverse(queryList), queryKey, { - stream: options.stream, - hydrate: options.hydrate - }) - } - - return this.cache.getDocuments(queryList, queryKey, { - stream: options.stream, - hydrate: options.hydrate - }) - } - - if (CACHE_MISS === verdict) { - const pageOptions = validatePaginationOpts(options) - const mongoQueryOptions = {} - - /* with pagination */ - if (pageOptions) { - mongoQueryOptions.skip = pageOptions.start - mongoQueryOptions.limit = pageOptions.size - - debug('using pagination', mongoQueryOptions) - } - - if (!options.stream) { - const documents = await this.mongo.findDocs( - queryKey, - mongoQueryOptions - ) - - /* cache query */ - this.cache.setDocuments(queryKey, documents) - - return documents - } - - const { sink, capture } = await this.mongo.findDocsStream( - queryKey, - mongoQueryOptions - ) - - /* wait for Mongo to finish streaming us docs, then do a cache operation */ - sink.on('end', () => { - /* only cache if we need to */ - if (capture.length > 0) this.cache.setDocuments(queryKey, capture) - }) - - return sink - } - - if (EMPTY_QUERY === verdict) { - /* no document(s) found */ - return null - } - } - - /** - * Fetch one MongoDB document from an ID string. - * - * @param {String} documentID - The `_id` of the requested document. - * - * @returns {Object|null} The document. - */ - async fetchById (documentID, options, collectionName = null) { - if (!options) options = {} - - const hex = /^[a-fA-F0-9]+$/ - if (!documentID || (documentID && !hex.test(documentID))) { - throw new FerretError( - 'InvalidArguments', - 'documentID is a required parameter and must be a valid 12-byte hexadecimal string or of type ObjectID', - 'fetch::fetchById', - { documentID } - ) - } - - if (documentID.constructor === ObjectID) { - documentID = documentID.toHexString() - } - - if (documentID.constructor !== String) { - return new FerretError( - 'InvalidOptions', - 'documentID must be of type String or ObjectID', - 'client::fetchById', - { documentID } - ) - } - - const queryKey = new QueryKey( - this.mongo.dbName, - collectionName || this.mongo.collectionName, - { _id: documentID }, - null - ) - - const document = await this.cache.getDocument( - documentID, - queryKey.collectionName - ) - - if (document == null) { - debug('cache miss', { _id: documentID }) - - let doc = await this.mongo.findDocs(queryKey) - if (!doc || (doc && !doc._id)) { - doc = null - } - - this.cache.setDocument(doc, documentID, queryKey.collectionName) - - return doc - } - - debug('cache hit', { _id: documentID }) - if (document === NULL_DOC.description) { - return null - } - - const parsed = options.hydrate ? hydrate(document) : JSON.parse(document) - return parsed - } - - /** - * Fetch the first MongoDB document from a query. - * - * @param {Object} [query={}] - An optional cursor query object. - * - * @returns {Object|null} The document. - */ - async fetchOne (query, options, collectionName) { - if (!options) options = {} - - const queryKey = new QueryKey( - this.mongo.dbName, - collectionName || this.mongo.collectionName, - query, - null - ) - - const oneKey = queryKey.oneKey() - const queryString = queryKey.queryString() - - const documentID = await this.cache.getQueryHash(oneKey, queryString) - - if (!documentID) { - debug('cache miss', oneKey, queryString) - let document = await this.mongo.findOne(queryKey) - - if (!document || !document._id) { - document = null - } - - const documentID = document - ? document._id.toHexString() - : EMPTY_QUERY.description - - this.cache.setQueryHash(oneKey, queryString, documentID) - if (document) { - this.cache.setDocument(document, null, queryKey.collectionName) - } - - return document - } - - debug('cache hit', oneKey, queryString) - if (documentID === EMPTY_QUERY.description) { - return null - } - - const document = await this.cache.getDocument( - documentID, - queryKey.collectionName - ) - - if (document === NULL_DOC.description) { - return null - } - - const parsed = options.hydrate ? hydrate(document) : JSON.parse(document) - return parsed - } -} - -module.exports = FireFerret diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index f5d6bd8..0000000 --- a/lib/error.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict' - -/** - * Creates a new FireFerret Error - * @constructor - * @param {name} [name='FireFerretError'] - The error name - * @param {msg} [msg=''] - The error message. - * @param {scope} [scope=''] - The scope where the error was thrown. - * @param {error} [error=null] - The original error message, invalid options, or invalid argument list. - * @returns {FireFerretError} - */ -class FerretError extends Error { - constructor (name, msg, scope, error) { - super(msg) - this.name = name || 'FireFerretError' - this.msg = msg || '' - this.scope = scope || '' - - if (error) this._error = error - } -} - -/** - * Error to string. - * @returns {string} - The FireFerretError as a string. - */ -FerretError.prototype.toString = function () { - const obj = Object(this) - if (obj !== this) throw new TypeError() - - return ( - `${this.name}: ${this.msg}` + - (this.scope ? `\n${this.scope}` : '') + - (this._error ? `\n${this._error}` : '') - ) -} - -FerretError.prototype.inspect = function () { - return this.toString() -} - -module.exports.FerretError = FerretError diff --git a/lib/fireferret.js b/lib/fireferret.js new file mode 100644 index 0000000..56a9d2b --- /dev/null +++ b/lib/fireferret.js @@ -0,0 +1,296 @@ +'use strict' + +const MongoDriver = require('./mongod') +const queryKey = require('./util/queryKey') +const hash = require('./util/hash') +const cacheManager = require('cache-manager') +const { promisify } = require('util') + +const log = require('util').debuglog('ff::client') + +class FireFerretClient { + constructor (opts = {}) { + this.mongod = new MongoDriver(opts.uri, { coll: opts.coll }) + this.activeDb = null + this.activeCollection = opts.coll || opts.collection || null + + this.cache = cacheManager.caching({ + store: opts.store, + driver: opts.driver || null, + host: opts.host, + port: opts.port, + db: opts.db, + ttl: opts.ttl + }) + } + + async connect () { + const [dbName, collName] = await this.mongod.connect() + this.activeDb = dbName + this.activeCollection = collName + + const collText = this.activeCollection ? ` with collection=${this.activeCollection}` : '' + log(`🔥🐈 FireFerret connected to db=${this.activeDb}${collText}`) + + return 'ok' + } + + async close () { + await this.mongod.close() + + if (this.cache.store.getClient) { + const cacheClient = this.cache.store.getClient(() => process.exit(0)) + + if (cacheClient && cacheClient.quit) { + cacheClient.quit() + return 'ok' + } + + if (cacheClient && cacheClient.close) { + cacheClient.close() + return 'ok' + } + } + + return 'ok' + } + + async find (query, opts = {}, collectionName = null) { + // reduce expensive mongodb lookups + + const loadFromDB = async () => { + const docs = await this.mongod.find(query, { skip: low, limit: size }) + + // O(# of docs) + const hmap = {}; const idmap = {} + docs.forEach(doc => { + const id = doc._id.toHexString() + const hashId = hash(id) + + if (hmap[hashId]) hmap[hashId].push(doc) + else hmap[hashId] = [doc] + + if (idmap[hashId]) idmap[hashId].push(id) + else idmap[hashId] = [id] + }) + + // O(# of docs / 512) + const hashes = Object.keys(hmap) + hashes.forEach(bucketKey => { + // set docs (ordered) inside buckets by hash + this.cache.set(bucketKey, hmap[bucketKey]) + }) + + // set query contents + this.cache.set(qK, idmap) // no need to wait + + log('🔥❌ Cache miss!') + return docs + } + + const loadFromCache = async () => { + const idmap = await this.cache.get(match); const buckets = Object.keys(idmap); const docs = [] + let partialFulfillment = false + + // O(# of docs / 512) + for (const bucketKey of buckets) { + const bucketConents = await this.cache.get(bucketKey) + + // bucket DNE; + if (!bucketConents || bucketConents.length === 0) { + partialFulfillment = true + break + } + + // if we need the whole bucket, there is no need to filter! + if (idmap[bucketKey].length === 512) { + docs.push(...bucketConents) + continue + } + + // filter out docs that are only included in the query + // array to dict for constant lookup + const bucketDict = {} + bucketConents.forEach(doc => { + bucketDict[doc._id] = doc + }) + + // all doc ids in the query + for (const id of idmap[bucketKey]) { + if (bucketDict[id]) docs.push(bucketDict[id]) + else { + partialFulfillment = true + break + } + } + + // break out early to avoid extra work + if (partialFulfillment) break + } + + // partial fulfillment is worthless, reload entire query from DB + if (partialFulfillment) return loadFromDB() + + log('🔥✅ Cache hit!') + return docs + } + + const loadSubsetFromCache = async () => { + const idmap = await this.cache.get(match); const bucketKeys = Object.keys(idmap); const docs = []; const loadOp = {} + + const desiredLen = high - low + let currLen = 0; let [remainingOffset] = matchDiff; let remaining = desiredLen + + for (const bucketKey of bucketKeys) { + if (desiredLen <= currLen) break + + const bucketLen = idmap[bucketKey].length + + if (remainingOffset >= bucketLen) { + remainingOffset -= bucketLen + + continue + } + + if (remainingOffset !== 0 && remainingOffset < bucketLen) { + const start = remainingOffset + const end = remaining < bucketLen - remainingOffset ? start + remaining : undefined + + const segmentLen = end ? end - start : bucketLen - start + currLen += segmentLen + remainingOffset = 0 + remaining -= segmentLen + + const ids = idmap[bucketKey].slice(start, end) + loadOp[bucketKey] = ids + + continue + } + + // add all of bucket + if (remainingOffset === 0 && remaining >= bucketLen) { + loadOp[bucketKey] = idmap[bucketKey] + + remaining -= bucketLen + currLen += bucketLen + + continue + } + + if (remainingOffset === 0 && remaining < bucketLen) { + const ids = idmap[bucketKey].slice(0, remaining + 1) + loadOp[bucketKey] = ids + + remaining = 0 + currLen += remaining + + continue + } + } + + let partialFulfillment = false + + for (const bucketKey of Object.keys(loadOp)) { + const bucketConents = await this.cache.get(bucketKey) + + // bucket DNE; + if (!bucketConents || bucketConents.length === 0) { + partialFulfillment = true + break + } + + // if we need the whole bucket, there is no need to filter! + if (loadOp[bucketKey].length === 512) { + docs.push(...bucketConents) + continue + } + + // filter out docs that are only included in the query + // array to dict for constant lookup + const bucketDict = {} + bucketConents.forEach(doc => { + bucketDict[doc._id] = doc + }) + + // all doc ids in the query + for (const id of loadOp[bucketKey]) { + if (bucketDict[id]) docs.push(bucketDict[id]) + else { + partialFulfillment = true + break + } + } + + // break out early to avoid extra work + if (partialFulfillment) break + } + + // partial fulfillment is worthless, reload entire query from DB + if (partialFulfillment) return loadFromDB() + + this.cache.set(qK, loadOp) + log(`🔥🧙 Found cached superset! Using query '${match}'`) + + return docs + } + + const [page, size] = opts.pg || [null, null] + const low = page && size ? (page - 1) * size : null; const high = page && size ? low + size : null + const qK = queryKey(this.activeDb, this.activeCollection, query, [low, high]) + + const keys = await promisify(this.cache.keys)() // ask cache for all keys + const strQ = JSON.stringify(query) + let match; let matchDiff = [Infinity, Infinity] + let wideMatch, wideMatchDiff + + // determine best match (superset), if it exists + if (keys) { + // absolute worst case O(2^(24-9)) = O(32768 iterations) + for (const key of keys) { + if (key === qK) { + match = key + break + } + + // match to subset + const [kDb, kColl, kQ, kR] = key.split('::') + + if (!kColl || !kQ) continue // is not a query key + + // entire set was cached previous, we can get a subset from this easily + if (kDb === this.activeDb && kColl === this.activeCollection && kQ === strQ && !kR) { + wideMatch = key + wideMatchDiff = [low, high] + } + + const [kLow, kHigh] = kR.split('-') + if ( + kDb === this.activeDb && + kColl === (collectionName || this.activeCollection) && kQ === strQ && + low >= kLow && + high <= kHigh && + (low - kLow < matchDiff[0] && kHigh - high < matchDiff[1]) + ) { + match = key + matchDiff = [(low - kLow), (kHigh - high)] + } + } + + if (!match && wideMatch) { + match = wideMatch + matchDiff = wideMatchDiff + } + } + + // no match found; read-through to DB. + if (!match) return loadFromDB() + + // match found; load it from the cache! + if (match === qK) return loadFromCache() + + // query not in cache, but a valid subset is! + if (match && match !== qK) return loadSubsetFromCache() + } +} + +module.exports = FireFerretClient diff --git a/lib/key.js b/lib/key.js deleted file mode 100644 index 0459774..0000000 --- a/lib/key.js +++ /dev/null @@ -1,152 +0,0 @@ -'use strict' - -const { FerretError } = require('./error') -const { - SPECIAL_CHARS: { QUERY_DELIMITER }, - validatePaginationOpts -} = require('./utils') - -/** - * Creates a new QueryKey instance. - * @constructor - * @param {String} dbName - The database to query - * @param {String} collectionName - The collection to query - * @param {Object} [query={}] - The cursor query object. - * @param {Object} [queryOptions=null] - The query options. - * @param {String} [ns='ff'] - The namescape to use for keys. - */ -class QueryKey { - constructor ( - dbName, - collectionName, - query = {}, - queryOptions = null, - ns = 'ff' - ) { - this.dbName = dbName - this.collectionName = collectionName - if (query) this.query = query - if (queryOptions) this.queryOptions = queryOptions - if (ns) this.ns = ns - - /* INTERNAL -- cache stringifies */ - this._queryString = null - this._toString = null - this._baseKey = null - this._oneKey = null - } - - /** - * Generates a QueryKey string without query options - * - * @returns {String} The base key string. - */ - baseKey () { - if (!this._baseKey) { - this._baseKey = this.toString(true) - } - - return this._baseKey - } - - /** - * Generates a QueryKey string to be used with individual retrievals. - * - * @returns {String} The one key string. - */ - oneKey () { - if (!this._oneKey) { - this._oneKey = `${this.ns}:${this.dbName}::${this.collectionName}:findOne` - } - - return this._oneKey - } - - /** - * Generate a QueryKey string which includes only the query itself. - * - * @returns {String} The Query string. - */ - queryString () { - if (!this._queryString) { - this._queryString = JSON.stringify( - this.query, - (k, value) => { - if (value.constructor.name === 'RegExp') { - /* Redis doesnt like backslashes in keys */ - return encodeURI(value.toString()) - } - return value - }, - 0 - ) - } - - return this._queryString - } - - /** - * Generates a QueryKey string. - * - * @param {boolean} [baseKeyOnly=false] - Generate only the base key variant. - * - * @returns {String} The QueryKey string. - */ - toString (baseKeyOnly = false) { - if (this._toString && !baseKeyOnly) return this._toString - - try { - const stringQuery = JSON.stringify( - this.query, - (k, value) => { - if (value.constructor.name === 'RegExp') { - /* Redis doesnt like backslashes in keys */ - return encodeURI(value.toString()) - } - return value - }, - 0 - ) - - /* Full key */ - if ( - !baseKeyOnly && - this.queryOptions && - Object.keys(this.queryOptions).length !== 0 - ) { - const options = {} - - const pageOptions = validatePaginationOpts(this.queryOptions) - - if (pageOptions) { - options.start = pageOptions.start - options.end = pageOptions.end - } - - const stringFilteredOpts = JSON.stringify(options) - - const stringKey = `${this.ns}:${this.dbName}::${this.collectionName}:query${QUERY_DELIMITER}${stringQuery}::${stringFilteredOpts}` - this._toString = stringKey - - return stringKey - } - - /* Base key */ - const stringKey = `${this.ns}:${this.dbName}::${this.collectionName}:query${QUERY_DELIMITER}${stringQuery}` - return stringKey - } catch (err) { - throw new FerretError( - 'InternalError', - 'key toString has failed.' + err, - 'QueryKey::toString', - err - ) - } - } - - inspect () { - return this.toString() - } -} - -module.exports = { QueryKey } diff --git a/lib/mongo.js b/lib/mongo.js deleted file mode 100644 index 8083d4a..0000000 --- a/lib/mongo.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict' - -const pump = require('pump') -const through = require('through2') -const { MongoClient } = require('mongodb') -const { PassThrough } = require('stream') - -const { FerretError } = require('./error') - -const debug = require('util').debuglog('fireferret::mongo') - -/* internal */ -let _client = null -let _options = {} -let _db = null -let _collection = null - -class Mongo { - constructor (uri, options, clientOptions) { - _options = options - _client = new MongoClient(uri, clientOptions) - - this.dbName = _options.dbName || '' - this.collectionName = _options.collectionName || '' - } - - /** - * Internal - Using this may induce unintended behaviors. - * @param {MongoClient} client - A npm::mongodb client - */ - _setClient (client) { - if (client) _client = client - } - - /** - * Internal - Using this may induce unintended behaviors. - * @returns An npm::mongodb client. - */ - _getClient () { - return _client - } - - /** - * Attempts to establish a connection to MongoDB. - */ - async connect () { - try { - const client = await _client.connect() - _client = client - _db = this.dbName ? _client.db(this.dbName) : _client.db() - - if (this.collectionName && _db) { - _collection = await _db.collection(_options.collectionName) - } - - debug( - `mongo client connected to: db:${_options.dbName} with collection:${_options.collectionName}` - ) - } catch (err) { - throw new FerretError( - 'ConnectionError', - 'Unable to connect to MongoDB resource.', - 'mongo::connect', - err - ) - } - } - - /** - * Attempts to close the connection to MongoDB. - */ - async close () { - try { - const reply = await _client.close() - - debug('mongo client closed successfully') - - return reply - } catch (err) { - throw new FerretError('ConnectionError', '', err) - } - } - - /** - * Select and return documents from a collection. - * @param {QueryKey} queryKey - A QueryKey instance. - * @param {Object} options - Query options. - * @returns {Array} An array of JSON documents. - */ - async findDocs (queryKey, options = {}) { - const query = queryKey.query - - try { - /* using the default collection */ - if (queryKey.collectionName === this.collectionName) { - return _collection.find(query, options).toArray() - } - - /* using passed collectionName */ - return _db - .collection(queryKey.collectionName) - .find(query, options) - .toArray() - } catch (err) { - throw new FerretError( - 'MongoError', - 'unable to find documents', - 'mongo::findDocs', - err - ) - } - } - - /** - * Select and return a stream of documents from a collection. - * @param {QueryKey} queryKey - A QueryKey instance. - * @param {Object} options - Query options. - * @returns {Object} A readable stream and array of captured stream documents. - */ - async findDocsStream (queryKey, options = {}) { - const query = queryKey.query - - const source = - queryKey.collectionName === this.collectionName - ? _collection.find(query, options) - : _db.collection(queryKey.collectionName).find(query, options) - - let fresh = true - const capture = [] - const xform = through.ctor( - { objectMode: true }, - transform, - _options.ndJSON ? null : flush - )() - const sink = PassThrough() - - pump(source, xform, sink) - - return { sink, capture } - - function transform (document, enc, cb) { - /* for caching internal caching operation */ - capture.push(document) - - const stringified = JSON.stringify(document) - if (stringified && _options.ndJSON) { - /* ndJSON spec */ - this.push(`${stringified}\n`) - } else if (stringified) { - /* array */ - this.push(`${(fresh ? '[' : ',') + stringified}`) - fresh = false - } else { - this.push(null) - } - cb() - } - - function flush (done) { - if (capture.length === 0) this.push('[') - this.push(']') - done() - } - } - - /** - * Select and return the first document from a collection. - * @param {QueryKey} queryKey - A QueryKey instance. - * @returns {Object} - */ - async findOne (queryKey) { - const query = queryKey.query - - try { - if (queryKey.collection !== this.collectionName) { - return _db.collection(queryKey.collectionName).findOne(query) - } - return _collection.findOne(query) - } catch (err) { - throw new FerretError('MongoError', err.msg, 'mongo::findById', err) - } - } -} - -module.exports = Mongo diff --git a/lib/mongod.js b/lib/mongod.js new file mode 100644 index 0000000..8481ad1 --- /dev/null +++ b/lib/mongod.js @@ -0,0 +1,42 @@ +'use strict' + +const { MongoClient } = require('mongodb') +const log = require('util').debuglog('ff::mongod') + +class MongoDriver { + constructor (uri, opts = {}) { + this.client = new MongoClient(uri, { useUnifiedTopology: true, ...opts }) + this.db = null + this.dbName = null + + if (opts.coll || opts.collection) this.collName = opts.coll ? opts.coll : opts.collection + } + + async connect () { + if (!this.client) throw new Error('Client DNE') + + await this.client.connect() + this.db = this.client.db() + this.dbName = this.db.databaseName + + log('🔥🥬 Connected to Mongod!') + + return [this.dbName, this.collName] + } + + async close () { + const reply = await this.client.close() + + log('🔥🥬 Connect to Mongod closed successfully!') + + return reply + } + + async find (query, opts = {}) { + log('🔥🥬 Find docs!') + + return this.db.collection(this.collName).find(query, opts).toArray() + } +} + +module.exports = MongoDriver diff --git a/lib/options.js b/lib/options.js deleted file mode 100644 index df7adb1..0000000 --- a/lib/options.js +++ /dev/null @@ -1,126 +0,0 @@ -'use strict' - -/** - * @namespace Options - */ - -/** - * @memberof Options - * - * @typedef {Object} QueryOptions - * - * @property {Boolean} [stream=false] - Format the query response as a stream. - * @property {Object} [pagination=null] Pagination options. - * @property {Number|String} pagination.page=null - The page number, starting at 1. - * @property {Number|String} pagination.size=null - The page size, greater than 0. - * - * @example - * { stream: true, pagination: { page: 2, size: 50 } } - */ - -/** - * @memberof Options - * - * @typedef {Object} FireFerretOptions - * - * @property {Options.Mongo} mongo - * @property {Options.Redis} redis - * @property {boolean} wideMatch=false - Use the Wide-Match strategy when checking the cache for queries. - */ - -/** - * @memberof Options - * - * @typedef {Object} Mongo - Dictates MongoDB driver behavior. Additional Driver documentation can be found {@link http://mongodb.github.io/node-mongodb-native/|here}. - * - * @property {String} uri - The MongoDB URI. - * @property {String} dbName - The name of the MongoDB database to connect to. - * @property {String} collectionName=null - The default database collection. - * @property {Boolean} ndJSON=false - Stream documents using the ndJSON spec. - * @property {String} encoding="utf-8" - Encoding to use when streaming. - */ - -/** - * @memberof Options - * - * @typedef {Object} Redis - Dictates Redis client behavior. Additional Client documentation can be found {@link https://www.npmjs.com/package/redis#options-object-properties|here}. - * - * @property {Boolean} ndJSON=false - Stream documents using the ndJSON spec. - * @property {Number} connectionTimeout=5000 - Timeout in milliseconds when connecting. - * @property {Number} count=1000 - Default Redis SCAN work amount. - * @property {Number} batchSize=1000 - Default batch size when using LPUSH. - */ - -const DEFAULTS = { - cache: { - wideMatch: true - }, - mongo: { - ndJSON: false, - encoding: 'utf-8', - dbName: '', - collectionName: '', - uri: '' - }, - mongoClient: { - useUnifiedTopology: true, - useNewUrlParser: true - }, - redisClient: { - ndJSON: false, - count: 1000, - connectionTimeout: 5000, - batchSize: 1000, - host: '127.0.0.1', - port: 6379, - path: null, - url: null, - string_numbers: null, - return_buffers: false, - detect_buffers: false, - socket_keepalive: true, - socket_initial_delay: 0, - no_ready_check: false, - enable_offline_queue: true, - retry_unfilfilled_commands: false, - password: null, - auth_pass: null, - db: null, - family: 'IPv4', - disable_resubscribing: null, - rename_commands: null, - tls: null, - prefix: null, - retry_strategy: null - } -} - -/** - * @memberof Options - * - * Generate client specific options by filtering out invalid fields. - * - * @param {Object} source - The source options to generate from. - * @param {String} name - Name of the client/driver the options are for. - * @param {boolean} [setDefaults=true] - If a source value is not present, use the default value. - * - * @returns {Object} Client specific options. - */ -function generateOptions (source, name, setDefaults = true) { - if (!source) return {} - - const options = {} - - const keys = Object.keys(DEFAULTS[name]) - for (let i = 0; i < keys.length; i++) { - const key = keys[i] - if (source[key]) options[key] = source[key] - else if (setDefaults && DEFAULTS[name][key] !== null) { - options[key] = DEFAULTS[name][key] - } - } - - return options -} - -module.exports = { generateOptions } diff --git a/lib/redis.js b/lib/redis.js deleted file mode 100644 index bb20060..0000000 --- a/lib/redis.js +++ /dev/null @@ -1,307 +0,0 @@ -'use strict' - -const pump = require('pump') -const { PassThrough } = require('stream') -const sliced = require('sliced') -const { createClient } = require('redis') -const { promisify } = require('util') - -const { FerretError } = require('./error') -const { fastReverse } = require('./utils') - -const debug = require('util').debuglog('fireferret::redis') - -/* internal */ -let _client = null -let _options = {} - -class RedisClient { - constructor (options) { - _options = options - } - - setClient (client) { - _client = client - } - - getClient () { - if (_client) return _client - } - - async connect () { - _client = createClient(_options) - - _client.on('ready', () => { - debug('redis server info', _client.server_info) - }) - - return new Promise((resolve, reject) => { - _client.on('connect', () => { - clearTimeout(timeout) - - debug('redis client connected') - - resolve('ok') - }) - - const timeout = setTimeout(() => { - reject( - new FerretError( - 'ConnectionError', - 'failed to connected before timeout', - 'redis::connect', - { timeout: _options.connectionTimeout } - ) - ) - }, _options.connectionTimeout) - }) - } - - async close () { - const _quit = promisify(_client.quit).bind(_client) - - try { - const reply = await _quit() - - debug('redis client closed successfully') - - return reply - } catch (err) { - throw new FerretError( - 'ConnectionError', - 'close operation has failed -- quit must be invoked', - 'redis::close', - err - ) - } - } - - /* Single operation wrappers */ - async hget (hash, field) { - const _hget = promisify(_client.hget).bind(_client) - - try { - return _hget(hash, field) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hget operation has failed', - 'redis::hget', - err - ) - } - } - - async hmget (key, fields) { - const _hmget = promisify(_client.hmget).bind(_client) - - try { - return _hmget(key, fields) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hmget operation has failed', - 'redis::hmget', - err - ) - } - } - - async hset (hash, args) { - const _hset = promisify(_client.hset).bind(_client) - - try { - await _hset(hash, args) - } catch (err) { - debug(err) - throw new FerretError( - 'RedisError', - 'hset operation has failed', - 'redis::hset', - err - ) - } - } - - async hmset (key, args) { - const _hmset = promisify(_client.hmset).bind(_client) - - try { - await _hmset(key, args) - } catch (err) { - throw new FerretError( - 'RedisError', - 'hmset operation has failed', - 'redis::hmset', - err - ) - } - } - - async lrange (key, start, end) { - const _lrange = promisify(_client.lrange).bind(_client) - - try { - if (start || end) { - start = start || 0 - end = end || -1 - - return _lrange(key, start, end) - } - - return _lrange(key, 0, -1) - } catch (err) { - throw new FerretError( - 'RedisError', - 'lrange operation has failed', - 'redis::lrange', - err - ) - } - } - - async lpush (key, elements, reverse = true) { - if (!key || key.length === 0) { - throw new FerretError( - 'InvalidArguments', - 'valid key is required for lpush', - 'redis::lpush', - { key } - ) - } - - if (!elements || elements.length === 0) { - throw new FerretError( - 'InvalidArguments', - 'Elements are required for lpush', - 'redis::lpush', - { elements } - ) - } - - const _lpush = promisify(_client.lpush).bind(_client) - if (reverse) fastReverse(elements) - - try { - const reply = await _lpush(key, ...elements) - - return reply - } catch (err) { - throw new FerretError( - 'RedisError', - 'lpush operation has failed', - 'redis::lpush', - err - ) - } - } - - async scan (cursor, pattern, count = _options.count) { - if ((!cursor && cursor !== 0) || !pattern) { - throw new FerretError( - 'RedisError', - 'cursor and pattern are required parameters for a SCAN operation', - 'redis::scan', - { cursor, pattern } - ) - } - - const _scan = promisify(_client.scan).bind(_client) - - try { - const elements = _scan(cursor, 'MATCH', pattern, 'COUNT', count) - - return elements - } catch (err) { - throw new FerretError( - 'RedisError', - 'scan operation has failed', - 'redis::scan', - err - ) - } - } - - /* Async multi operations */ - async multihmget (operations, stream) { - const hashes = Object.keys(operations) - - if (!stream) { - const multi = _client.multi() - - for (let i = 0; i < hashes.length; i++) { - multi.hmget(hashes[i], operations[hashes[i]]) - } - - const exec = promisify(multi.exec).bind(multi) - - try { - const collation = await exec() - - return collation - } catch (err) { - throw new FerretError( - 'RedisError', - 'one or more hgetall operations have failed', - 'redis::multihmget', - err - ) - } - } - - /* using streams */ - const source = PassThrough() - const sink = PassThrough() - - let fresh = true - for ( - let i = 0, hash = hashes[i]; - i < hashes.length; - i++, hash = hashes[i] - ) { - this.hmget(hash, operations[hash]).then((bucket) => { - source.write( - `${ - (fresh && !_options.ndJSON ? '[' : '') + - bucket.join(!_options.ndJSON ? ',' : '\n') - }` - ) - fresh = false - - /* if this is the last bucket, end the stream appropriately */ - if (_options.ndJSON && i === hashes.length - 1) source.end() - if (!_options.ndJSON && i === hashes.length - 1) source.end(']') - else if (!_options.ndJSON) { - /* 'join' the batches together (array return) */ - source.write(',') - } - }) - } - - pump(source, sink) - - return sink - } - - /* Batch operations wrap single operation */ - async batchlpush (key, elements, batchSize = _options.batchSize) { - if (batchSize > elements.length) return this.lpush(key, elements, true) - - const elementCount = elements.length - const batchOps = [] - - /* retain document parity with mongo */ - fastReverse(elements) - - for (let i = 0; i < Math.ceil(elementCount / batchSize); i += 1) { - const end = - elementCount < (i + 1) * batchSize ? elementCount : (i + 1) * batchSize - const batch = sliced(elements, i * batchSize, end) - if (batch.length > 0) batchOps.push(this.lpush(key, batch, false)) - } - - return Promise.all(batchOps) - } -} - -module.exports = RedisClient diff --git a/lib/util/hash.js b/lib/util/hash.js new file mode 100644 index 0000000..aafef19 --- /dev/null +++ b/lib/util/hash.js @@ -0,0 +1,15 @@ +'use strict' + +const ID_LEN = 24; const BUCKET_MAX = 512 + +// calculate hash from mongo object id +module.exports = function hash (id) { + if (id && (typeof id === 'string' || id instanceof String) && id.length === ID_LEN) { + const counter = parseInt(id.slice(18), 16) + const quotient = Math.floor(counter / BUCKET_MAX) + + return quotient.toString() + } + + throw new Error(`Unable to calculate hash value; invalid object id: ${id}`) +} diff --git a/lib/util/queryKey.js b/lib/util/queryKey.js new file mode 100644 index 0000000..cd44d86 --- /dev/null +++ b/lib/util/queryKey.js @@ -0,0 +1,13 @@ +'use strict' + +module.exports = function queryKey (dbName, collName, query = {}, range) { + if (!dbName || !collName) throw new Error('dbName and collName are required!') + + const strQ = JSON.stringify(query) + + const [low, high] = range + const formattedRange = low && high ? `${low}-${high}` : '' + + const raw = `${dbName}::${collName}::${strQ}::${formattedRange}` + return raw +} diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index 7b64d0f..0000000 --- a/lib/utils.js +++ /dev/null @@ -1,204 +0,0 @@ -'use strict' - -/** - * @namespace Utils - */ - -const { FerretError } = require('./error') -const { ObjectID } = require('mongodb') - -const debug = require('util').debuglog('fireferret::utils') - -const SYMBOLS = { - EMPTY_QUERY: Symbol('EMPTY_QUERY'), - EMPTY_OBJ: Symbol('EMPTY_OBJECT'), - NULL_DOC: Symbol('NULL_DOC'), - CACHE_HIT: Symbol('CACHE_HIT'), - CACHE_MISS: Symbol('CACHE_MISS') -} - -const SPECIAL_CHARS = { - QUERY_DELIMITER: '=', - BUCKET_DELIMITER: '~' -} - -/** - * Pagination Options - * - * @typedef PaginationOptions - * @property {Number} page - The page number. - * @property {Number} size - The page size. - * @property {Number} start - The starting index. - * @property {Number} end - The ending index (exclusive) - */ -/** - * Validates and reformats pagination options. - * @memberof Utils - * - * @param {Object} options - The query options. - * - * @returns {PaginationOptions|null} - */ -function validatePaginationOpts (options) { - if (!options) options = {} - - let { page, size } = options.pagination ? options.pagination : {} - - if (page && size) { - if (isNaN(page) || isNaN(size)) { - throw new FerretError( - 'InvalidOptions', - 'Pagination requires page and size to be numbers', - 'page::validatePaginationOpts', - options.pagination - ) - } - - page = Number(page) - size = Number(size) - - if (page === 0) { - throw new FerretError( - 'InvalidOptions', - 'page is zero -- paginagion begins with page = 1', - 'page::validatePaginationOpts', - options.pagination - ) - } - - const start = (page - 1) * size - const end = page * size - - return { page, size, start, end } - } - - return null -} - -/** - * Wide-Match strategy results. - * - * @typedef WideMatch - * @property {String} targetQuery - A super-query that contains the requested query. - * @property {Object} rangeOptions - * @property {Number} rangeOptions.start - The starting index of the requested query's data (inclusive). - * @property {Number} rangeOptions.end - The starting index of the requested query's data (exclusive). - */ -/** - * Determines if any perviously cached queries can be used to fulfill the current query. - * @memberof Utils - * - * @param {Array} cachedQueries - A list of previously cached queries. - * @param {PaginationOptions} pageOptions - Pagination options for the current query, if any. - * - * @returns {WideMatch|null} - */ -function filterAvailableWideMatch (cachedQueries, pageOptions) { - if (!cachedQueries || cachedQueries.constructor.name !== 'Array') { - return null - } - - /* sort by shortest key */ - const sorted = cachedQueries.sort((a, b) => { - return a.length - b.length - }) - - /* push non-paginated query if it exists */ - if (sorted[0].split(/::/).length === 2) sorted.push(sorted.shift()) - - for (const query of sorted) { - const split = query.split(/::/) - - /* grab the params from the query, if they exist */ - const queryParams = split[2] - ? split[2].length > 0 - ? split[2] - : null - : null - - /* prefer to use smallest superset i.e. another paginated query */ - if (queryParams) { - const { start, end } = JSON.parse(queryParams) - - /* goldilocks zone; not too hot, not too cold */ - if (start <= pageOptions.start && end >= pageOptions.end) { - const _start = pageOptions.start - start - const _end = _start + (pageOptions.end - pageOptions.start) - - const ret = { - targetQuery: query, - rangeOptions: { - start: _start, - end: _end - } - } - - debug('WideMatch - Goldilocks criteria met:', ret) - return ret - } - } - - /* mass look-up matched */ - if (!queryParams) { - const ret = { - targetQuery: query, - rangeOptions: pageOptions - } - return ret - } - } - - /* failed to match any existing query */ - return null -} - -/* @AamuLumi https://github.com/kb-dev/sanic.js */ -function fastReverse (array) { - let temp = null - const length = array.length - - for (let i = 0, max = Math.floor(length / 2); i < max; i++) { - temp = array[i] - array[i] = array[length - i - 1] - array[length - i - 1] = temp - } - - return array -} - -/** - * When parsing, coerce types where applicable. - * @param {Object} document - The document to hydrate. - * - * @returns {Object} The hydrated document. - */ -function hydrate (document) { - const dateFormat = /^(.)(\d*)-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/ - try { - return JSON.parse(document, (key, value) => { - if (key === '_id') { - return ObjectID(value) - } else if (typeof value === 'string' && dateFormat.test(value)) { - return new Date(value) - } else { - return value - } - }) - } catch (err) { - throw new FerretError( - 'SyntaxError', - 'Unable to hydrate documents. Received unexpected JSON input.', - 'utils::hydrate', - err - ) - } -} - -module.exports = { - SYMBOLS, - SPECIAL_CHARS, - validatePaginationOpts, - filterAvailableWideMatch, - fastReverse, - hydrate -} diff --git a/package-lock.json b/package-lock.json index d87adbe..e59f9db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "fireferret", - "version": "0.4.4", + "version": "1.0.0-rc0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1285,6 +1285,11 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1600,6 +1605,16 @@ "unset-value": "^1.0.0" } }, + "cache-manager": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.4.4.tgz", + "integrity": "sha512-oayy7ukJqNlRUYNUfQBwGOLilL0X5q7GpuaF19Yqwo6qdx49OoTZKRIF5qbbr+Ru8mlTvOpvnMvVq6vw72pOPg==", + "requires": { + "async": "3.2.0", + "lodash": "^4.17.21", + "lru-cache": "6.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1628,12 +1643,12 @@ "dev": true }, "catharsis": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.11.tgz", - "integrity": "sha512-a+xUyMV7hD1BrDQA/3iPV7oc+6W26BgVJO05PGEoatMyIuPScQKsde6i3YorWX1qs+AZjnJ18NqdKoCtKiNh1g==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", "dev": true, "requires": { - "lodash": "^4.17.14" + "lodash": "^4.17.15" } }, "chalk": { @@ -3025,9 +3040,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -5124,25 +5139,25 @@ "dev": true }, "jsdoc": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.5.tgz", - "integrity": "sha512-SbY+i9ONuxSK35cgVHaI8O9senTE4CDYAmGSDJ5l3+sfe62Ff4gy96osy6OW84t4K4A8iGnMrlRrsSItSNp3RQ==", + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.7.tgz", + "integrity": "sha512-sxKt7h0vzCd+3Y81Ey2qinupL6DpRSZJclS04ugHDNmRUXGzqicMJ6iwayhSA0S0DwwX30c5ozyUthr1QKF6uw==", "dev": true, "requires": { "@babel/parser": "^7.9.4", "bluebird": "^3.7.2", - "catharsis": "^0.8.11", + "catharsis": "^0.9.0", "escape-string-regexp": "^2.0.0", "js2xmlparser": "^4.0.1", "klaw": "^3.0.0", "markdown-it": "^10.0.0", "markdown-it-anchor": "^5.2.7", - "marked": "^0.8.2", + "marked": "^2.0.3", "mkdirp": "^1.0.4", "requizzle": "^0.2.3", "strip-json-comments": "^3.1.0", "taffydb": "2.6.2", - "underscore": "~1.10.2" + "underscore": "~1.13.1" }, "dependencies": { "escape-string-regexp": { @@ -5156,12 +5171,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true - }, - "underscore": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.10.2.tgz", - "integrity": "sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg==", - "dev": true } } }, @@ -5364,8 +5373,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.sortby": { "version": "4.7.0", @@ -5388,6 +5396,14 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -5449,9 +5465,9 @@ "dev": true }, "marked": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", - "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-2.1.1.tgz", + "integrity": "sha512-5XFS69o9CzDpQDSpUYC+AN2xvq8yl1EGa5SG/GI1hP78/uTeo3PDfiDNmsUyiahpyhToDDJhQk7fNtJsga+KVw==", "dev": true }, "matflap": { @@ -7983,9 +7999,9 @@ } }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.0.tgz", + "integrity": "sha512-6ezXvzOZupqKj4jUqbQ9tXuJNo+BR2gU8fFRk3XCP3e0G6WT414u5ELe6Y0vtp7kmSJ3F7YWObSNr1ESsgi4vw==", "dev": true }, "xml-name-validator": { @@ -8018,6 +8034,11 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "yargs": { "version": "15.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", diff --git a/package.json b/package.json index 11ce51d..9f2e29a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fireferret", - "version": "0.4.4", + "version": "1.0.0-rc0", "description": "Autocaching query client for MongoDB, with powerful filtering functionality.", "main": "index.js", "files": [ @@ -34,6 +34,7 @@ "testRegex": "./tests/.*/test..*.js$" }, "dependencies": { + "cache-manager": "^3.4.4", "matflap": "^0.0.3", "mongodb": "^3.5.9", "pump": "^3.0.0", @@ -46,7 +47,7 @@ "coveralls": "^3.1.0", "jaguarjs-jsdoc": "^1.1.0", "jest": "^26.4.2", - "jsdoc": "^3.6.5", + "jsdoc": "^3.6.7", "mockuments": "0.0.3", "mongodb-memory-server": "^6.6.7", "redis-mock": "^0.51.0",