nano-memoize
Advanced tools
Comparing version 0.0.1-a to 0.0.4-b
'use strict'; | ||
/*MIT License | ||
Core benchmark code copied from micro-memoize | ||
Copyright (c) 2018 Tony Quetano | ||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. | ||
*/ | ||
const Benchmark = require('benchmark'); | ||
@@ -16,132 +40,7 @@ const Table = require('cli-table2'); | ||
const moize = require('moize'); | ||
const microMemoize = require('../dist/micro-memoize.js').default; //require('../lib').default; | ||
const microMemoize = require('micro-memoize').default; //require('../lib').default; | ||
const iMemoized = require('iMemoized'); | ||
const nanomemoize = require('../src/nano-memoize.js'); | ||
/*function micromemo (fn, options={}) { | ||
const {equals,maxAge,maxArgs,serializer} = options, | ||
memoized = resolver.bind(this,fn,{},serializer,equals,maxAge); | ||
return memoized; | ||
} | ||
function resolver (fn,singles,serializer=JSON.stringify,equals,maxAge,arg) { | ||
if(!equals && arguments.length<=6) { | ||
const cacheKey = typeof arg === 'string' || typeof arg === 'function' || typeof arg === 'object' ? serializer(arg) : arg; | ||
return singles[cacheKey] || (singles[cacheKey]=fn.call(this, arg)); | ||
} | ||
const args = [].slice.call(arguments,5), | ||
result = {}; | ||
equals || (equals = (a,b) => a===b); | ||
for(let i=0;i<keys.length;i++) { | ||
if(keys[i]===null) { result.index = i; continue; } | ||
const key = maxArgs ? keys[i].slice(0,maxArgs) : keys[i]; | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; | ||
if(j===max) { | ||
result.index = i; | ||
result.value = values[i]; | ||
} | ||
} | ||
} | ||
} | ||
const i = result.index>=0 ? result.index : values.length; | ||
if(maxAge) { | ||
if(timeouts[i]) clearTimeout(timeouts[i]); | ||
timeouts[i] = setTimeout(() => keys[i]=values[i]=timeouts[i]=null,maxAge) | ||
} | ||
return typeof(result.value)==="undefined" ? values[i] = fn.call(this,...(keys[i] = args)) : result.value; | ||
};*/ | ||
function micromemo (fn, options={}) { | ||
const {serializer=JSON.stringify,equals,maxAge,maxArgs,maxSize,stats} = options, | ||
singles = {}, | ||
keys = [], | ||
values = [], | ||
changes = [], | ||
change = (cache,key,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
changes[key] = {key,cache}; | ||
}, | ||
hits = [], | ||
hit = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
let record = hits[key]; | ||
if(!record) record = hits[key] = {count:0,cache}; | ||
hit.count++; | ||
hit.time = Date.now(); | ||
}, | ||
timeouts = [], | ||
timeout = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
if(timeouts[key]) clearTimeout(timeouts[key]); | ||
timeouts[key] = setTimeout(() => cache[key]=timeouts[key]=null,maxAge); | ||
}; | ||
let memoized; | ||
if(fn.length===1 && !equals) { | ||
memoized = single.bind( | ||
this, | ||
fn, | ||
singles, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
serializer | ||
); | ||
} else { | ||
memoized = multiple.bind( | ||
this, | ||
fn, | ||
keys, | ||
values, | ||
serializer, | ||
equals ? equals : (a,b) => a===b, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
maxArgs | ||
); | ||
} | ||
memoized.clear = () => { | ||
Object.keys(singles).forEach(key => delete singles[key]); | ||
keys.splice(0,keys.length); | ||
values.splice(0,values.length); | ||
changes.splice(0,changes.length); | ||
Object.keys(changes).forEach(key => key==="length" || delete changes[key]); | ||
timeouts.forEach(timeout => !timeout || clearTimeout(timeout)); | ||
timeouts.splice(0,timeouts.length); | ||
Object.keys(timeouts).forEach(key => key==="length" || delete timeouts[key]); | ||
} | ||
return memoized; | ||
} | ||
function single (f,cache,change,serializer,arg) { | ||
if(arguments.length<=5) { | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" ? arg : serializer(arg)); | ||
if(change) change(key,true); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
} | ||
function multiple(f,keys,values,serializer,equals,change,maxArgs,...args) { | ||
const result = {}; | ||
for(let i=0;i<keys.length;i++) { | ||
let key = keys[i]; | ||
if(key===null) { result.index = i; continue; } | ||
if(maxArgs) key = key.slice(0,maxArgs); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; | ||
if(j===max) { | ||
result.index = i; | ||
result.value = values[i]; | ||
} | ||
} | ||
} | ||
} | ||
const i = result.index>=0 ? result.index : values.length; | ||
if(change) change(key,true); | ||
return typeof result.value === "undefined" ? result.value = values[i] = f(...(keys[i] = args)) : result.value; | ||
} | ||
const deepEquals = require('lodash').isEqual; | ||
@@ -249,3 +148,3 @@ const fastDeepEqual = require('fast-equals').deepEqual; | ||
const mIMemoized = iMemoized.memoize(fibonacci); | ||
const mMicro = micromemo(fibonacci); | ||
const mNano = nanomemoize(fibonacci); | ||
@@ -255,3 +154,3 @@ | ||
fibonacciSuite | ||
/* .add('addy-osmani', () => { | ||
.add('addy-osmani', () => { | ||
mAddyOsmani(fibonacciNumber); | ||
@@ -278,3 +177,3 @@ }) | ||
mIMemoized(fibonacciNumber); | ||
})*/ | ||
}) | ||
.add('micro-memoize', () => { | ||
@@ -289,8 +188,8 @@ mMicroMemoize(fibonacciNumber); | ||
}) | ||
.add('micromemo', () => { | ||
mMicro(fibonacciNumber); | ||
.add('namomemoize', () => { | ||
mNano(fibonacciNumber); | ||
}) | ||
.on('start', () => { | ||
console.log(''); // eslint-disable-line no-console | ||
console.log('Starting cycles for functions with a single parameter...'); // eslint-disable-line no-console | ||
console.log('Starting cycles for functions with a single primitive parameter...'); // eslint-disable-line no-console | ||
@@ -312,2 +211,76 @@ results = []; | ||
const runSingleParameterObjectSuite = () => { | ||
const fibonacciSuite = new Benchmark.Suite('Single parameter'); | ||
const fibonacciNumber = Number(35); | ||
const mUnderscore = underscore(fibonacci); | ||
const mLodash = lodash(fibonacci); | ||
const mRamda = ramda(fibonacci); | ||
const mMemoizee = memoizee(fibonacci); | ||
const mFastMemoize = fastMemoize(fibonacci); | ||
const mAddyOsmani = addyOsmani(fibonacci); | ||
const mMemoizerific = memoizerific(Infinity)(fibonacci); | ||
const mLruMemoize = lruMemoize(Infinity)(fibonacci); | ||
const mMoize = moize(fibonacci); | ||
const mMicroMemoize = microMemoize(fibonacci); | ||
const mIMemoized = iMemoized.memoize(fibonacci); | ||
const mNano = nanomemoize(fibonacci); | ||
return new Promise((resolve) => { | ||
fibonacciSuite | ||
.add('addy-osmani', () => { | ||
mAddyOsmani(fibonacciNumber); | ||
}) | ||
.add('lodash', () => { | ||
mLodash(fibonacciNumber); | ||
}) | ||
.add('lru-memoize', () => { | ||
mLruMemoize(fibonacciNumber); | ||
}) | ||
.add('memoizee', () => { | ||
mMemoizee(fibonacciNumber); | ||
}) | ||
.add('memoizerific', () => { | ||
mMemoizerific(fibonacciNumber); | ||
}) .add('ramda', () => { | ||
mRamda(fibonacciNumber); | ||
}) | ||
.add('underscore', () => { | ||
mUnderscore(fibonacciNumber); | ||
}) | ||
.add('iMemoized', () => { | ||
mIMemoized(fibonacciNumber); | ||
}) | ||
.add('micro-memoize', () => { | ||
mMicroMemoize(fibonacciNumber); | ||
}) | ||
.add('moize', () => { | ||
mMoize(fibonacciNumber); | ||
}) | ||
.add('fast-memoize', () => { | ||
mFastMemoize(fibonacciNumber); | ||
}) | ||
.add('namomemoize', () => { | ||
mNano(fibonacciNumber); | ||
}) | ||
.on('start', () => { | ||
console.log(''); // eslint-disable-line no-console | ||
console.log('Starting cycles for functions with a single object parameter...'); // eslint-disable-line no-console | ||
results = []; | ||
spinner.start(); | ||
}) | ||
.on('cycle', onCycle) | ||
.on('complete', () => { | ||
onComplete(); | ||
resolve(); | ||
}) | ||
.run({ | ||
async: true | ||
}); | ||
}); | ||
}; | ||
const runMultiplePrimitiveSuite = () => { | ||
@@ -326,7 +299,7 @@ const fibonacciSuite = new Benchmark.Suite('Multiple parameters (Primitive)'); | ||
const mIMemoized = iMemoized.memoize(fibonacciMultiplePrimitive); | ||
const mMicro = micromemo(fibonacciMultiplePrimitive); | ||
const mNano = nanomemoize(fibonacciMultiplePrimitive); | ||
return new Promise((resolve) => { | ||
fibonacciSuite | ||
/*.add('addy-osmani', () => { | ||
.add('addy-osmani', () => { | ||
mAddyOsmani(fibonacciNumber, isComplete); | ||
@@ -345,3 +318,3 @@ }) | ||
mMemoizerific(fibonacciNumber, isComplete); | ||
})*/ | ||
}) | ||
.add('fast-memoize', () => { | ||
@@ -356,4 +329,4 @@ mFastMemoize(fibonacciNumber, isComplete); | ||
}) | ||
.add('micromemo', () => { | ||
mMicro(fibonacciNumber, isComplete); | ||
.add('nanomemoize', () => { | ||
mNano(fibonacciNumber, isComplete); | ||
}) | ||
@@ -393,7 +366,7 @@ .on('start', () => { | ||
const mMicroMemoize = microMemoize(fibonacciMultipleObject); | ||
const mMicro = micromemo(fibonacciMultipleObject); | ||
const mNano = nanomemoize(fibonacciMultipleObject); | ||
return new Promise((resolve) => { | ||
fibonacciSuite | ||
/* .add('addy-osmani', () => { | ||
.add('addy-osmani', () => { | ||
mAddyOsmani(fibonacciNumber, isComplete); | ||
@@ -409,3 +382,3 @@ }) | ||
mMemoizerific(fibonacciNumber, isComplete); | ||
})*/ | ||
}) | ||
.add('fast-memoize', () => { | ||
@@ -420,4 +393,4 @@ mFastMemoize(fibonacciNumber, isComplete); | ||
}) | ||
.add('micromemo', () => { | ||
mMicro(fibonacciNumber,isComplete); | ||
.add('nanomemoize', () => { | ||
mNano(fibonacciNumber,isComplete); | ||
}) | ||
@@ -461,6 +434,6 @@ .on('start', () => { | ||
const mMicroDeep = micromemo(fibonacciMultipleDeepEqual, { | ||
const mNanoDeep = nanomemoize(fibonacciMultipleDeepEqual, { | ||
equals: deepEquals | ||
}); | ||
const mMicroFastDeep = micromemo(fibonacciMultipleDeepEqual, { | ||
const mNanoFastDeep = nanomemoize(fibonacciMultipleDeepEqual, { | ||
equals: fastDeepEqual | ||
@@ -480,7 +453,7 @@ }); | ||
}) | ||
.add('micromemo deep equals (lodash isEqual)', () => { | ||
mMicroDeep(fibonacciNumber); | ||
.add('nanomemoize deep equals (lodash isEqual)', () => { | ||
mNanoDeep(fibonacciNumber); | ||
}) | ||
.add('micromemo deep equals (fast-equals deepEqual)', () => { | ||
mMicroFastDeep(fibonacciNumber); | ||
.add('nanomemoize deep equals (fast-equals deepEqual)', () => { | ||
mNanoFastDeep(fibonacciNumber); | ||
}) | ||
@@ -509,4 +482,5 @@ .on('start', () => { | ||
runSingleParameterSuite() | ||
.then(runSingleParameterObjectSuite) | ||
.then(runMultiplePrimitiveSuite) | ||
.then(runMultipleObjectSuite) | ||
.then(runAlternativeOptionsSuite); |
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | ||
(function() { | ||
"use strict"; | ||
const hasVargs = f => { | ||
const s = f+"", | ||
i = s.indexOf("..."); | ||
return i>=0 && i<s.indexOf(")" || s.indexOf("arguments")>=0); | ||
} | ||
function nanomemoize (fn, options={}) { | ||
const {serializer=JSON.stringify,equals,maxAge,maxArgs,maxSize,stats} = options, | ||
singles = {}, | ||
keys = [], | ||
values = [], | ||
changes = [], | ||
change = (cache,key,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
changes[key] = {key,cache}; | ||
const { | ||
serializer = (value) => { | ||
if(value && (typeof value === "string" || typeof value === "object")) { | ||
// strings must be stringified because cache[1] should not equal or overwrite cache["1"] for value = 1 and value = "1" | ||
return JSON.stringify(value); | ||
} | ||
return value; | ||
}, | ||
hits = [], | ||
hit = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
let record = hits[key]; | ||
if(!record) record = hits[key] = {count:0,cache}; | ||
hit.count++; | ||
hit.time = Date.now(); | ||
equals, | ||
maxAge, | ||
maxArgs, | ||
vargs = hasVargs(fn) | ||
} = options, | ||
s = {}, // single arg function key/value cache | ||
k = [], // multiple arg function arg key cache | ||
v = [], // multiple arg function result cache | ||
c = {}, // key change cache | ||
change = (cache,key) => { // logs key changes | ||
c[key] = {key,cache}; | ||
}, | ||
timeouts = [], | ||
timeout = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
if(timeouts[key]) clearTimeout(timeouts[key]); | ||
timeouts[key] = setTimeout(() => cache[key]=timeouts[key]=null,maxAge); | ||
t = {}, | ||
timeout = (change) => { // deletes timed-out keys | ||
if(t[change.key]) clearTimeout(t[change.key]); | ||
t[change.key] = setTimeout(() => { | ||
delete change.cache[change.key]; | ||
delete t[change.key]; | ||
},maxAge); | ||
}; | ||
let memoized; | ||
if(fn.length===1 && !equals) { | ||
memoized = single.bind( | ||
setInterval(() => { // process key changes out of cycle for speed | ||
for(let p in c) { | ||
if(maxAge) timeout(c[p]); | ||
delete c[p]; | ||
} | ||
},1); | ||
let f, | ||
unary = fn.length===1 && !equals && !vargs; | ||
// pre-bind core arguments, faster than using a closure or passing on stack | ||
if(unary) { | ||
f = single.bind( | ||
this, | ||
fn, | ||
singles, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
s, | ||
(maxAge ? change.bind(this,s): null), // turn change logging on and bind to arg cache s | ||
serializer | ||
); | ||
} else { | ||
memoized = multiple.bind( | ||
f = multiple.bind( | ||
this, | ||
fn, | ||
keys, | ||
values, | ||
serializer, | ||
equals ? equals : (a,b) => a===b, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
k, | ||
v, | ||
equals || ((a,b) => a===b), // default to just a regular strict comparison | ||
(maxAge ? change.bind(this,v): null), // turn change logging on and bind to arg cache v | ||
maxArgs | ||
); | ||
} | ||
memoized.clear = () => { | ||
Object.keys(singles).forEach(key => delete singles[key]); | ||
keys.splice(0,keys.length); | ||
values.splice(0,values.length); | ||
changes.splice(0,changes.length); | ||
Object.keys(changes).forEach(key => key==="length" || delete changes[key]); | ||
timeouts.forEach(timeout => !timeout || clearTimeout(timeout)); | ||
timeouts.splice(0,timeouts.length); | ||
Object.keys(timeouts).forEach(key => key==="length" || delete timeouts[key]); | ||
// reset all the caches, must splice arrays or delete keys on objects to retain bind integrity | ||
f.clear = () => { | ||
Object.keys(s).forEach(k => delete s[k]); | ||
k.splice(0,k.length); | ||
v.splice(0,v.length); | ||
Object.keys(c).forEach(k => delete c[k]); | ||
Object.keys(t).forEach(k => { clearTimeout(t[k]); delete t[k]; }); | ||
} | ||
memoized.keys = () => keys.slice(); | ||
memoized.values = () => values.slice(); | ||
memoized.keyValues = () => Object.assign({},singles); | ||
return memoized; | ||
f.keys = () => (!unary ? k.slice() : null); | ||
f.values = () => (!unary ? v.slice() : null); | ||
f.keyValues = () => (unary ? Object.assign({},s) : null); | ||
return f; | ||
} | ||
// for single argument functions, just use a JS object key look-up | ||
function single (f,cache,change,serializer,arg) { | ||
if(arguments.length<=5) { | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" ? arg : serializer(arg)); | ||
if(change) change(key,true); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" || arg.constructor===Number || arg.constructor===Boolean ? arg : serializer(arg)); | ||
if(change) change(key); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
function multiple(f,keys,values,serializer,equals,change,maxArgs,...args) { | ||
const result = {}; | ||
for(let i=0;i<keys.length;i++) { | ||
let key = keys[i]; | ||
if(key===null) { result.index = i; continue; } | ||
if(maxArgs) key = key.slice(0,maxArgs); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; | ||
if(j===max) { | ||
result.index = i; | ||
result.value = values[i]; | ||
} | ||
// for multiple arg functions, loop through a cache of all the args | ||
// looking at each arg separately so a test can abort as soon as possible | ||
function multiple(f,keys,values,equals,change,max,...args) { | ||
const rslt = {}; | ||
for(let i=0;i<keys.length;i++) { // an array of arrays of args | ||
let key = keys[i]; | ||
if(max) key = key.slice(0,max); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; // go to next key if args don't match | ||
if(j===max) { // the args matched | ||
rslt.index = i; | ||
rslt.value = values[i]; // get the cached value | ||
} | ||
} | ||
} | ||
const i = result.index>=0 ? result.index : values.length; | ||
if(change) change(key,true); | ||
return typeof result.value === "undefined" ? result.value = values[i] = f(...(keys[i] = args)) : result.value; | ||
} | ||
const i = rslt.index>=0 ? rslt.index : values.length; | ||
if(change) change(i); | ||
return typeof rslt.value === "undefined" ? rslt.value = values[i] = f(...(keys[i] = args)) : rslt.value; | ||
} | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
}).call(this); | ||
},{}]},{},[1]); |
@@ -1,1 +0,1 @@ | ||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){(function(){"use strict";function nanomemoize(fn,options={}){const{serializer:serializer=JSON.stringify,equals:equals,maxAge:maxAge,maxArgs:maxArgs,maxSize:maxSize,stats:stats}=options,singles={},keys=[],values=[],changes=[],change=(cache,key,property)=>{if(property)key=typeof key+"@"+key;changes[key]={key:key,cache:cache}},hits=[],hit=(key,cache,property)=>{if(property)key=typeof key+"@"+key;let record=hits[key];if(!record)record=hits[key]={count:0,cache:cache};hit.count++;hit.time=Date.now()},timeouts=[],timeout=(key,cache,property)=>{if(property)key=typeof key+"@"+key;if(timeouts[key])clearTimeout(timeouts[key]);timeouts[key]=setTimeout(()=>cache[key]=timeouts[key]=null,maxAge)};let memoized;if(fn.length===1&&!equals){memoized=single.bind(this,fn,singles,maxSize||maxAge||stats?change.bind(this,values):null,serializer)}else{memoized=multiple.bind(this,fn,keys,values,serializer,equals?equals:(a,b)=>a===b,maxSize||maxAge||stats?change.bind(this,values):null,maxArgs)}memoized.clear=(()=>{Object.keys(singles).forEach(key=>delete singles[key]);keys.splice(0,keys.length);values.splice(0,values.length);changes.splice(0,changes.length);Object.keys(changes).forEach(key=>key==="length"||delete changes[key]);timeouts.forEach(timeout=>!timeout||clearTimeout(timeout));timeouts.splice(0,timeouts.length);Object.keys(timeouts).forEach(key=>key==="length"||delete timeouts[key])});memoized.keys=(()=>keys.slice());memoized.values=(()=>values.slice());memoized.keyValues=(()=>Object.assign({},singles));return memoized}function single(f,cache,change,serializer,arg){if(arguments.length<=5){const key=!arg||typeof arg==="number"||typeof arg==="boolean"?arg:serializer(arg);if(change)change(key,true);return cache[key]||(cache[key]=f.call(this,arg))}}function multiple(f,keys,values,serializer,equals,change,maxArgs,...args){const result={};for(let i=0;i<keys.length;i++){let key=keys[i];if(key===null){result.index=i;continue}if(maxArgs)key=key.slice(0,maxArgs);if(key.length===args.length){const max=key.length-1;for(let j=0;j<=max;j++){if(!equals(key[j],args[j]))break;if(j===max){result.index=i;result.value=values[i]}}}}const i=result.index>=0?result.index:values.length;if(change)change(key,true);return typeof result.value==="undefined"?result.value=values[i]=f(...keys[i]=args):result.value}if(typeof module!=="undefined")module.exports=nanomemoize;if(typeof window!=="undefined")window.nanomemoize=nanomemoize}).call(this)},{}]},{},[1]); | ||
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){(function(){"use strict";const hasVargs=f=>{const s=f+"",i=s.indexOf("...");return i>=0&&i<s.indexOf(")"||s.indexOf("arguments")>=0)};function nanomemoize(fn,options={}){const{serializer:serializer=(value=>{if(value&&(typeof value==="string"||typeof value==="object")){return JSON.stringify(value)}return value}),equals:equals,maxAge:maxAge,maxArgs:maxArgs,vargs:vargs=hasVargs(fn)}=options,s={},k=[],v=[],c={},change=(cache,key)=>{c[key]={key:key,cache:cache}},t={},timeout=change=>{if(t[change.key])clearTimeout(t[change.key]);t[change.key]=setTimeout(()=>{delete change.cache[change.key];delete t[change.key]},maxAge)};setInterval(()=>{for(let p in c){if(maxAge)timeout(c[p]);delete c[p]}},1);let f,unary=fn.length===1&&!equals&&!vargs;if(unary){f=single.bind(this,fn,s,maxAge?change.bind(this,s):null,serializer)}else{f=multiple.bind(this,fn,k,v,equals||((a,b)=>a===b),maxAge?change.bind(this,v):null,maxArgs)}f.clear=(()=>{Object.keys(s).forEach(k=>delete s[k]);k.splice(0,k.length);v.splice(0,v.length);Object.keys(c).forEach(k=>delete c[k]);Object.keys(t).forEach(k=>{clearTimeout(t[k]);delete t[k]})});f.keys=(()=>!unary?k.slice():null);f.values=(()=>!unary?v.slice():null);f.keyValues=(()=>unary?Object.assign({},s):null);return f}function single(f,cache,change,serializer,arg){const key=!arg||typeof arg==="number"||typeof arg==="boolean"||arg.constructor===Number||arg.constructor===Boolean?arg:serializer(arg);if(change)change(key);return cache[key]||(cache[key]=f.call(this,arg))}function multiple(f,keys,values,equals,change,max,...args){const rslt={};for(let i=0;i<keys.length;i++){let key=keys[i];if(max)key=key.slice(0,max);if(key.length===args.length){const max=key.length-1;for(let j=0;j<=max;j++){if(!equals(key[j],args[j]))break;if(j===max){rslt.index=i;rslt.value=values[i]}}}}const i=rslt.index>=0?rslt.index:values.length;if(change)change(i);return typeof rslt.value==="undefined"?rslt.value=values[i]=f(...keys[i]=args):rslt.value}if(typeof module!=="undefined")module.exports=nanomemoize;if(typeof window!=="undefined")window.nanomemoize=nanomemoize}).call(this)},{}]},{},[1]); |
@@ -1,1 +0,1 @@ | ||
(function(){"use strict";function nanomemoize(fn,options={}){const{serializer:serializer=JSON.stringify,equals:equals,maxAge:maxAge,maxArgs:maxArgs,maxSize:maxSize,stats:stats}=options,singles={},keys=[],values=[],changes=[],change=(cache,key,property)=>{if(property)key=typeof key+"@"+key;changes[key]={key:key,cache:cache}},hits=[],hit=(key,cache,property)=>{if(property)key=typeof key+"@"+key;let record=hits[key];if(!record)record=hits[key]={count:0,cache:cache};hit.count++;hit.time=Date.now()},timeouts=[],timeout=(key,cache,property)=>{if(property)key=typeof key+"@"+key;if(timeouts[key])clearTimeout(timeouts[key]);timeouts[key]=setTimeout(()=>cache[key]=timeouts[key]=null,maxAge)};let memoized;if(fn.length===1&&!equals){memoized=single.bind(this,fn,singles,maxSize||maxAge||stats?change.bind(this,values):null,serializer)}else{memoized=multiple.bind(this,fn,keys,values,serializer,equals?equals:(a,b)=>a===b,maxSize||maxAge||stats?change.bind(this,values):null,maxArgs)}memoized.clear=(()=>{Object.keys(singles).forEach(key=>delete singles[key]);keys.splice(0,keys.length);values.splice(0,values.length);changes.splice(0,changes.length);Object.keys(changes).forEach(key=>key==="length"||delete changes[key]);timeouts.forEach(timeout=>!timeout||clearTimeout(timeout));timeouts.splice(0,timeouts.length);Object.keys(timeouts).forEach(key=>key==="length"||delete timeouts[key])});memoized.keys=(()=>keys.slice());memoized.values=(()=>values.slice());memoized.keyValues=(()=>Object.assign({},singles));return memoized}function single(f,cache,change,serializer,arg){if(arguments.length<=5){const key=!arg||typeof arg==="number"||typeof arg==="boolean"?arg:serializer(arg);if(change)change(key,true);return cache[key]||(cache[key]=f.call(this,arg))}}function multiple(f,keys,values,serializer,equals,change,maxArgs,...args){const result={};for(let i=0;i<keys.length;i++){let key=keys[i];if(key===null){result.index=i;continue}if(maxArgs)key=key.slice(0,maxArgs);if(key.length===args.length){const max=key.length-1;for(let j=0;j<=max;j++){if(!equals(key[j],args[j]))break;if(j===max){result.index=i;result.value=values[i]}}}}const i=result.index>=0?result.index:values.length;if(change)change(key,true);return typeof result.value==="undefined"?result.value=values[i]=f(...keys[i]=args):result.value}if(typeof module!=="undefined")module.exports=nanomemoize;if(typeof window!=="undefined")window.nanomemoize=nanomemoize}).call(this); | ||
(function(){"use strict";const hasVargs=f=>{const s=f+"",i=s.indexOf("...");return i>=0&&i<s.indexOf(")"||s.indexOf("arguments")>=0)};function nanomemoize(fn,options={}){const{serializer:serializer=(value=>{if(value&&(typeof value==="string"||typeof value==="object")){return JSON.stringify(value)}return value}),equals:equals,maxAge:maxAge,maxArgs:maxArgs,vargs:vargs=hasVargs(fn)}=options,s={},k=[],v=[],c={},change=(cache,key)=>{c[key]={key:key,cache:cache}},t={},timeout=change=>{if(t[change.key])clearTimeout(t[change.key]);t[change.key]=setTimeout(()=>{delete change.cache[change.key];delete t[change.key]},maxAge)};setInterval(()=>{for(let p in c){if(maxAge)timeout(c[p]);delete c[p]}},1);let f,unary=fn.length===1&&!equals&&!vargs;if(unary){f=single.bind(this,fn,s,maxAge?change.bind(this,s):null,serializer)}else{f=multiple.bind(this,fn,k,v,equals||((a,b)=>a===b),maxAge?change.bind(this,v):null,maxArgs)}f.clear=(()=>{Object.keys(s).forEach(k=>delete s[k]);k.splice(0,k.length);v.splice(0,v.length);Object.keys(c).forEach(k=>delete c[k]);Object.keys(t).forEach(k=>{clearTimeout(t[k]);delete t[k]})});f.keys=(()=>!unary?k.slice():null);f.values=(()=>!unary?v.slice():null);f.keyValues=(()=>unary?Object.assign({},s):null);return f}function single(f,cache,change,serializer,arg){const key=!arg||typeof arg==="number"||typeof arg==="boolean"||arg.constructor===Number||arg.constructor===Boolean?arg:serializer(arg);if(change)change(key);return cache[key]||(cache[key]=f.call(this,arg))}function multiple(f,keys,values,equals,change,max,...args){const rslt={};for(let i=0;i<keys.length;i++){let key=keys[i];if(max)key=key.slice(0,max);if(key.length===args.length){const max=key.length-1;for(let j=0;j<=max;j++){if(!equals(key[j],args[j]))break;if(j===max){rslt.index=i;rslt.value=values[i]}}}}const i=rslt.index>=0?rslt.index:values.length;if(change)change(i);return typeof rslt.value==="undefined"?rslt.value=values[i]=f(...keys[i]=args):rslt.value}if(typeof module!=="undefined")module.exports=nanomemoize;if(typeof window!=="undefined")window.nanomemoize=nanomemoize}).call(this); |
155
index.js
(function() { | ||
"use strict"; | ||
const hasVargs = f => { | ||
const s = f+"", | ||
i = s.indexOf("..."); | ||
return i>=0 && i<s.indexOf(")" || s.indexOf("arguments")>=0); | ||
} | ||
function nanomemoize (fn, options={}) { | ||
const {serializer=JSON.stringify,equals,maxAge,maxArgs,maxSize,stats} = options, | ||
singles = {}, | ||
keys = [], | ||
values = [], | ||
changes = [], | ||
change = (cache,key,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
changes[key] = {key,cache}; | ||
const { | ||
serializer = (value) => { | ||
if(value && (typeof value === "string" || typeof value === "object")) { | ||
// strings must be stringified because cache[1] should not equal or overwrite cache["1"] for value = 1 and value = "1" | ||
return JSON.stringify(value); | ||
} | ||
return value; | ||
}, | ||
hits = [], | ||
hit = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
let record = hits[key]; | ||
if(!record) record = hits[key] = {count:0,cache}; | ||
hit.count++; | ||
hit.time = Date.now(); | ||
equals, | ||
maxAge, | ||
maxArgs, | ||
vargs = hasVargs(fn) | ||
} = options, | ||
s = {}, // single arg function key/value cache | ||
k = [], // multiple arg function arg key cache | ||
v = [], // multiple arg function result cache | ||
c = {}, // key change cache | ||
change = (cache,key) => { // logs key changes | ||
c[key] = {key,cache}; | ||
}, | ||
timeouts = [], | ||
timeout = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
if(timeouts[key]) clearTimeout(timeouts[key]); | ||
timeouts[key] = setTimeout(() => cache[key]=timeouts[key]=null,maxAge); | ||
t = {}, | ||
timeout = (change) => { // deletes timed-out keys | ||
if(t[change.key]) clearTimeout(t[change.key]); | ||
t[change.key] = setTimeout(() => { | ||
delete change.cache[change.key]; | ||
delete t[change.key]; | ||
},maxAge); | ||
}; | ||
let memoized; | ||
if(fn.length===1 && !equals) { | ||
memoized = single.bind( | ||
setInterval(() => { // process key changes out of cycle for speed | ||
for(let p in c) { | ||
if(maxAge) timeout(c[p]); | ||
delete c[p]; | ||
} | ||
},1); | ||
let f, | ||
unary = fn.length===1 && !equals && !vargs; | ||
// pre-bind core arguments, faster than using a closure or passing on stack | ||
if(unary) { | ||
f = single.bind( | ||
this, | ||
fn, | ||
singles, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
s, | ||
(maxAge ? change.bind(this,s): null), // turn change logging on and bind to arg cache s | ||
serializer | ||
); | ||
} else { | ||
memoized = multiple.bind( | ||
f = multiple.bind( | ||
this, | ||
fn, | ||
keys, | ||
values, | ||
serializer, | ||
equals ? equals : (a,b) => a===b, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
k, | ||
v, | ||
equals || ((a,b) => a===b), // default to just a regular strict comparison | ||
(maxAge ? change.bind(this,v): null), // turn change logging on and bind to arg cache v | ||
maxArgs | ||
); | ||
} | ||
memoized.clear = () => { | ||
Object.keys(singles).forEach(key => delete singles[key]); | ||
keys.splice(0,keys.length); | ||
values.splice(0,values.length); | ||
changes.splice(0,changes.length); | ||
Object.keys(changes).forEach(key => key==="length" || delete changes[key]); | ||
timeouts.forEach(timeout => !timeout || clearTimeout(timeout)); | ||
timeouts.splice(0,timeouts.length); | ||
Object.keys(timeouts).forEach(key => key==="length" || delete timeouts[key]); | ||
// reset all the caches, must splice arrays or delete keys on objects to retain bind integrity | ||
f.clear = () => { | ||
Object.keys(s).forEach(k => delete s[k]); | ||
k.splice(0,k.length); | ||
v.splice(0,v.length); | ||
Object.keys(c).forEach(k => delete c[k]); | ||
Object.keys(t).forEach(k => { clearTimeout(t[k]); delete t[k]; }); | ||
} | ||
memoized.keys = () => keys.slice(); | ||
memoized.values = () => values.slice(); | ||
memoized.keyValues = () => Object.assign({},singles); | ||
return memoized; | ||
f.keys = () => (!unary ? k.slice() : null); | ||
f.values = () => (!unary ? v.slice() : null); | ||
f.keyValues = () => (unary ? Object.assign({},s) : null); | ||
return f; | ||
} | ||
// for single argument functions, just use a JS object key look-up | ||
function single (f,cache,change,serializer,arg) { | ||
if(arguments.length<=5) { | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" ? arg : serializer(arg)); | ||
if(change) change(key,true); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" || arg.constructor===Number || arg.constructor===Boolean ? arg : serializer(arg)); | ||
if(change) change(key); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
function multiple(f,keys,values,serializer,equals,change,maxArgs,...args) { | ||
const result = {}; | ||
for(let i=0;i<keys.length;i++) { | ||
let key = keys[i]; | ||
if(key===null) { result.index = i; continue; } | ||
if(maxArgs) key = key.slice(0,maxArgs); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; | ||
if(j===max) { | ||
result.index = i; | ||
result.value = values[i]; | ||
} | ||
// for multiple arg functions, loop through a cache of all the args | ||
// looking at each arg separately so a test can abort as soon as possible | ||
function multiple(f,keys,values,equals,change,max,...args) { | ||
const rslt = {}; | ||
for(let i=0;i<keys.length;i++) { // an array of arrays of args | ||
let key = keys[i]; | ||
if(max) key = key.slice(0,max); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; // go to next key if args don't match | ||
if(j===max) { // the args matched | ||
rslt.index = i; | ||
rslt.value = values[i]; // get the cached value | ||
} | ||
} | ||
} | ||
const i = result.index>=0 ? result.index : values.length; | ||
if(change) change(key,true); | ||
return typeof result.value === "undefined" ? result.value = values[i] = f(...(keys[i] = args)) : result.value; | ||
} | ||
const i = rslt.index>=0 ? rslt.index : values.length; | ||
if(change) change(i); | ||
return typeof rslt.value === "undefined" ? rslt.value = values[i] = f(...(keys[i] = args)) : rslt.value; | ||
} | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
}).call(this); | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
}).call(this); | ||
{ | ||
"name": "nano-memoize", | ||
"version": "v0.0.1a", | ||
"description": "Faster than fast, smaller than micro ... a nano speed and size memoizer.", | ||
"version": "v0.0.4b", | ||
"description": "Faster than fast, smaller than micro ... a nano speed and nano size memoizer.", | ||
"engines": {}, | ||
"license": "MIT", | ||
"scripts": { | ||
"prepare": "cp ./src/nano-memoize.js ./index.js && browserify ./src/nano-memoize.js -o browser/nano-memoize.js && uglifyjs browser/nano-memoize.js -o browser/nano-memoize.min.js && uglifyjs src/nano-memoize.js -o dist/nano-memoize.min.js" | ||
"prepare": "cp ./src/nano-memoize.js ./index.js && cp ./src/nano-memoize.js dist/nano-memoize.js && browserify ./src/nano-memoize.js -o browser/nano-memoize.js && uglifyjs browser/nano-memoize.js -o browser/nano-memoize.min.js && uglifyjs src/nano-memoize.js -o dist/nano-memoize.min.js" | ||
}, | ||
@@ -30,2 +30,3 @@ "repository": { | ||
"chai": "^3.4.1", | ||
"cli-table2": "^0.2.0", | ||
"codacy-coverage": "^2.0.0", | ||
@@ -46,8 +47,7 @@ "codeclimate-test-reporter": "^0.2.0", | ||
"moize": "^4.0.4", | ||
"ora": "^1.3.0", | ||
"ramda": "^0.25.0", | ||
"underscore": "^1.8.3" | ||
}, | ||
"dependencies": { | ||
} | ||
"dependencies": {} | ||
} |
121
README.md
@@ -1,19 +0,128 @@ | ||
# Faster than fast, smaller than micro ... a nano speed and size memoizer. | ||
# Faster than fast, smaller than micro ... nano-memoizer. | ||
# Introduction | ||
The great devs [caiogondim](https://github.com/caiogondim) and [planttheidea](https://github.com/planttheidea) have produced consistently fast memoizers. We analyzed their code to see if we could build something faster than [fast-memoize](https://github.com/caiogondim/fast-memoize.js) and smaller than [micro-memoize](https://github.com/planttheidea/micro-memoize) while adding back some of the functionality [moize](https://github.com/planttheidea/moize). We think we have done it. The test results below are from: | ||
The devs [caiogondim](https://github.com/caiogondim) and [planttheidea](https://github.com/planttheidea) have produced great memoizers. We analyzed their code to see if we could build something faster than [fastmemoize](https://github.com/caiogondim/fastmemoize.js) and smaller than [micromemoize](https://github.com/planttheidea/micromemoize) while adding back some of the functionality of [moize](https://github.com/planttheidea/moize) removed in micro-memoize. We think we have done it ... but credit to them ... we just merged the best ideas in both and eliminated excess code. | ||
Along the way we discovered some size and speed optimizations that could be made to [fast-memoize](https://github.com/caiogondim/fast-memoize.js) and will be making a pull request to [caiogondim](https://github.com/caiogondim) for those of you desiring to stick with a tried and true solution. | ||
The minified/gzipped size is 920 bytes for `nano-memoize` vs 959 bytes for `micro-memoize`. | ||
The speed tests are below. | ||
`fast-memoize` is only the fastest in one case, single argument functions taking an object. However, in the case of a single primitve argument `nano-memoize` is 240% faster and in all other cases even better than that. | ||
`micro-memoize` is faster than `fast-memoize` except for single argument functions. | ||
`nano-memoize` is always faster than `moize`. | ||
`nano-memoize` and `fast-memoize` are comparable for multiple-argument functions. `fast-memoize` is always the slowest for these. | ||
We have found that benchmarks can vary dramartically from O/S to O/S or node version to node version. These tests were run on a Windows 10 64bit 2.4gx machine with 8GB RAM and Node v9.4.0. | ||
Functions with a single primitive parameter... | ||
| Name� � � � � | Ops / sec� �| Relative margin of error | Sample size | | ||
|---------------|-------------|--------------------------|-------------| | ||
| nano-memoize �| 183,478,889 |� �0.55%� � � � � � � � � | 90� � � � � | | ||
| fast-memoize� | 75,218,544� |� �2.21%� � � � � � � � � | 81� � � � � | | ||
| micro-memoize | 26,565,887� |� �1.48%� � � � � � � � � | 79� � � � � | | ||
| moize� � � � �| 22,047,750� |� �0.48%� � � � � � � � � | 86� � � � � | | ||
Functions with a single object parameter... | ||
| Name� � � � � | Ops / sec� | Relative margin of error | Sample size | | ||
|---------------|------------|--------------------------|-------------| | ||
| fast-memoize� | 78,297,395 |� �0.54%� � � � � � � � � | 90� � � � � | | ||
| nano-memoize �| 57,453,837 |� �2.03%� � � � � � � � � | 86� � � � � | | ||
| micro-memoize | 26,615,102 |� �0.40%� � � � � � � � � | 91� � � � � | | ||
| moize� � � � �| 21,760,403 |� �0.53%� � � � � � � � � | 84� � � � � | | ||
Functions with multiple parameters that contain only primitives... | ||
| Name� � � � � | Ops / sec� | Relative margin of error | Sample size | | ||
|---------------|------------|--------------------------|-------------| | ||
| nanomemoize� �| 18,280,535 |� �1.45%� � � � � � � � � | 83� � � � � | | ||
| micro-memoize | 16,394,987 |� �0.95%� � � � � � � � � | 86� � � � � | | ||
| moize� � � � �| 13,921,400 |� �0.93%� � � � � � � � � | 87� � � � � | | ||
| fast-memoize� | 884,771� � |� �0.47%� � � � � � � � � | 94� � � � � | | ||
Functions with multiple parameters that contain objects... | ||
| Name� � � � � | Ops / sec� | Relative margin of error | Sample size | | ||
|---------------|------------|--------------------------|-------------| | ||
| micro-memoize | 16,244,817 |� �0.86%� � � � � � � � � | 89� � � � � | | ||
| nanomemoize� �| 14,362,298 |� �1.89%� � � � � � � � � | 89� � � � � | | ||
| moize� � � � �| 14,123,028 |� �0.62%� � � � � � � � � | 88� � � � � | | ||
| fast-memoize� | 682,549� � |� �0.49%� � � � � � � � � | 90� � � � � | | ||
Deep equals ... | ||
| Name� � � � � � � � � � � � � � � � � � � � � � � | Ops / sec� | Relative margin of error | Sample size | | ||
|---------------------------------------------------|------------|--------------------------|-------------| | ||
| micro-memoize deep equals (hash-it isEqual)� � � �| 18,894,707 |� �0.86%� � � � � � � � � | 91� � � � � | | ||
| nanomemoize deep equals (lodash isEqual)� � � � � | 18,504,364 |� �0.85%� � � � � � � � � | 90� � � � � | | ||
| micro-memoize deep equals (fast-equals deepEqual) | 16,302,930 |� �0.73%� � � � � � � � � | 87� � � � � | | ||
| nanomemoize deep equals (fast-equals deepEqual)� �| 15,162,874 |� �0.83%� � � � � � � � � | 89� � � � � | | ||
We were puzzled about the multiple argument performance on `fast-memoize` given its stated goal of being the "fastest possible". We discovered that the default caching and serialization approach used by fast-memoize only performs well for single argument functions for two reasons: | ||
1) It uses `toJSON` to create a key for an entire argument list. This can be slow. | ||
2) Because a single key is generated for all arguments when perhaps only the first argument differs in a call, a lot of extra work is done. The `moize` and `micro-memoize` approach adopted by `nano-memoize` is far faster for multiple arguments. | ||
Along the way we also discovered that fast-memoize is subject to a key generation risk on edge case functions and fixed the flaw. The fork is [here](https://github.com/anywhichway/fastmemoize.js). We have submitted a pull request. See this [Medium article](https://codeburst.io/akeytokeyswhenjavascriptkeysdontmatchab44c81adc87) for details. | ||
# Usage | ||
nmp install nano-memoize | ||
use the code in the `browser` directory for the browser | ||
Since most devs are running a build pipeline, the code is not transpiled, although it is browserified | ||
# API | ||
The API is a subset of the `moize` API. Documentation coming. | ||
The API is a subset of the `moize` API. | ||
# Release History (rverse chronological order) | ||
```javascript | ||
const memoized = micromemoize(sum(a,b) => a + b); | ||
memoized(1,2); // 3 | ||
memoized(1,2); // pulled from cache | ||
``` | ||
2018-01-24 v0.0.1a - First public release. Benchmark code in repository not yet running. | ||
`memoized(function,options) returns function` | ||
The shape of options is: | ||
```javascript | ||
{ | ||
maxArgs: number, // only use the provided maxArgs for cache look-up, useful for ignoring final callback arguments | ||
maxAge: number, // number of milliseconds to cache a result | ||
serializer: function, // the serializer/key generator to use for single argument functions (multi-argument functions do not use a serializer) | ||
equals: function, // the equals function to use for multi-argument functions, e.g. deepEquals for objects (single-argument functions use serializer not equals) | ||
vargs: boolean // forces the use of multi-argument paradigm, auto set if function has a spread argument or uses `arguments` in its body. | ||
} | ||
``` | ||
# Release History (reverse chronological order) | ||
2018-01-27 v0.0.4b BETA Fixed benchmarks. Removed maxSize. More unit tests. Fixed maxAge. | ||
2018-01-27 v0.0.3b BETA More unit tests. Documentation. Benchmark code in repository not yet running. | ||
2018-01-24 v0.0.2a ALPHA Minor speed enhancements. Benchmark code in repository not yet running. | ||
2018=01-24 v0.0.1a ALPHA First public release. Benchmark code in repository not yet running. |
(function() { | ||
"use strict"; | ||
const hasVargs = f => { | ||
const s = f+"", | ||
i = s.indexOf("..."); | ||
return i>=0 && i<s.indexOf(")" || s.indexOf("arguments")>=0); | ||
} | ||
function nanomemoize (fn, options={}) { | ||
const {serializer=JSON.stringify,equals,maxAge,maxArgs,maxSize,stats} = options, | ||
singles = {}, | ||
keys = [], | ||
values = [], | ||
changes = [], | ||
change = (cache,key,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
changes[key] = {key,cache}; | ||
const { | ||
serializer = (value) => { | ||
if(value && (typeof value === "string" || typeof value === "object")) { | ||
// strings must be stringified because cache[1] should not equal or overwrite cache["1"] for value = 1 and value = "1" | ||
return JSON.stringify(value); | ||
} | ||
return value; | ||
}, | ||
hits = [], | ||
hit = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
let record = hits[key]; | ||
if(!record) record = hits[key] = {count:0,cache}; | ||
hit.count++; | ||
hit.time = Date.now(); | ||
equals, | ||
maxAge, | ||
maxArgs, | ||
vargs = hasVargs(fn) | ||
} = options, | ||
s = {}, // single arg function key/value cache | ||
k = [], // multiple arg function arg key cache | ||
v = [], // multiple arg function result cache | ||
c = {}, // key change cache | ||
change = (cache,key) => { // logs key changes | ||
c[key] = {key,cache}; | ||
}, | ||
timeouts = [], | ||
timeout = (key,cache,property) => { | ||
if(property) key = typeof(key) + "@" + key; | ||
if(timeouts[key]) clearTimeout(timeouts[key]); | ||
timeouts[key] = setTimeout(() => cache[key]=timeouts[key]=null,maxAge); | ||
t = {}, | ||
timeout = (change) => { // deletes timed-out keys | ||
if(t[change.key]) clearTimeout(t[change.key]); | ||
t[change.key] = setTimeout(() => { | ||
delete change.cache[change.key]; | ||
delete t[change.key]; | ||
},maxAge); | ||
}; | ||
let memoized; | ||
if(fn.length===1 && !equals) { | ||
memoized = single.bind( | ||
setInterval(() => { // process key changes out of cycle for speed | ||
for(let p in c) { | ||
if(maxAge) timeout(c[p]); | ||
delete c[p]; | ||
} | ||
},1); | ||
let f, | ||
unary = fn.length===1 && !equals && !vargs; | ||
// pre-bind core arguments, faster than using a closure or passing on stack | ||
if(unary) { | ||
f = single.bind( | ||
this, | ||
fn, | ||
singles, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
s, | ||
(maxAge ? change.bind(this,s): null), // turn change logging on and bind to arg cache s | ||
serializer | ||
); | ||
} else { | ||
memoized = multiple.bind( | ||
f = multiple.bind( | ||
this, | ||
fn, | ||
keys, | ||
values, | ||
serializer, | ||
equals ? equals : (a,b) => a===b, | ||
(maxSize || maxAge || stats ? change.bind(this,values): null), | ||
k, | ||
v, | ||
equals || ((a,b) => a===b), // default to just a regular strict comparison | ||
(maxAge ? change.bind(this,v): null), // turn change logging on and bind to arg cache v | ||
maxArgs | ||
); | ||
} | ||
memoized.clear = () => { | ||
Object.keys(singles).forEach(key => delete singles[key]); | ||
keys.splice(0,keys.length); | ||
values.splice(0,values.length); | ||
changes.splice(0,changes.length); | ||
Object.keys(changes).forEach(key => key==="length" || delete changes[key]); | ||
timeouts.forEach(timeout => !timeout || clearTimeout(timeout)); | ||
timeouts.splice(0,timeouts.length); | ||
Object.keys(timeouts).forEach(key => key==="length" || delete timeouts[key]); | ||
// reset all the caches, must splice arrays or delete keys on objects to retain bind integrity | ||
f.clear = () => { | ||
Object.keys(s).forEach(k => delete s[k]); | ||
k.splice(0,k.length); | ||
v.splice(0,v.length); | ||
Object.keys(c).forEach(k => delete c[k]); | ||
Object.keys(t).forEach(k => { clearTimeout(t[k]); delete t[k]; }); | ||
} | ||
memoized.keys = () => keys.slice(); | ||
memoized.values = () => values.slice(); | ||
memoized.keyValues = () => Object.assign({},singles); | ||
return memoized; | ||
f.keys = () => (!unary ? k.slice() : null); | ||
f.values = () => (!unary ? v.slice() : null); | ||
f.keyValues = () => (unary ? Object.assign({},s) : null); | ||
return f; | ||
} | ||
// for single argument functions, just use a JS object key look-up | ||
function single (f,cache,change,serializer,arg) { | ||
if(arguments.length<=5) { | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" ? arg : serializer(arg)); | ||
if(change) change(key,true); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
const key = (!arg || typeof arg === "number" || typeof arg ==="boolean" || arg.constructor===Number || arg.constructor===Boolean ? arg : serializer(arg)); | ||
if(change) change(key); | ||
return cache[key] || ( cache[key] = f.call(this, arg)); | ||
} | ||
function multiple(f,keys,values,serializer,equals,change,maxArgs,...args) { | ||
const result = {}; | ||
for(let i=0;i<keys.length;i++) { | ||
let key = keys[i]; | ||
if(key===null) { result.index = i; continue; } | ||
if(maxArgs) key = key.slice(0,maxArgs); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; | ||
if(j===max) { | ||
result.index = i; | ||
result.value = values[i]; | ||
} | ||
// for multiple arg functions, loop through a cache of all the args | ||
// looking at each arg separately so a test can abort as soon as possible | ||
function multiple(f,keys,values,equals,change,max,...args) { | ||
const rslt = {}; | ||
for(let i=0;i<keys.length;i++) { // an array of arrays of args | ||
let key = keys[i]; | ||
if(max) key = key.slice(0,max); | ||
if(key.length===args.length) { | ||
const max = key.length - 1; | ||
for(let j=0;j<=max;j++) { | ||
if(!equals(key[j],args[j])) break; // go to next key if args don't match | ||
if(j===max) { // the args matched | ||
rslt.index = i; | ||
rslt.value = values[i]; // get the cached value | ||
} | ||
} | ||
} | ||
const i = result.index>=0 ? result.index : values.length; | ||
if(change) change(key,true); | ||
return typeof result.value === "undefined" ? result.value = values[i] = f(...(keys[i] = args)) : result.value; | ||
} | ||
const i = rslt.index>=0 ? rslt.index : values.length; | ||
if(change) change(i); | ||
return typeof rslt.value === "undefined" ? rslt.value = values[i] = f(...(keys[i] = args)) : rslt.value; | ||
} | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
}).call(this); | ||
if(typeof(module)!=="undefined") module.exports = nanomemoize; | ||
if(typeof(window)!=="undefined") window.nanomemoize = nanomemoize; | ||
}).call(this); | ||
@@ -22,2 +22,5 @@ var chai, | ||
varArg = nanomemoize((...args) => args); | ||
describe("Test",function() { | ||
@@ -31,2 +34,48 @@ it("single primitive arg cached",function() { | ||
}); | ||
it("single object arg cached",function() { | ||
const value = {p1:1}, | ||
result = singleArg(value); | ||
expect(result).to.equal(value); | ||
}); | ||
it("multiple arg primitive cached",function() { | ||
const result = multipleArg(1,2); | ||
expect(result.arg1).to.equal(1); | ||
expect(result.arg2).to.equal(2); | ||
}); | ||
it("multiple arg object cached",function() { | ||
const arg1 = {arg:1}, | ||
arg2 = {arg:2}, | ||
result = multipleArg(arg1,arg2); | ||
expect(result.arg1.arg).to.equal(1); | ||
expect(result.arg2.arg).to.equal(2); | ||
}); | ||
it("multiple arg works with single",function() { | ||
const arg1 = {arg:1}; | ||
result = multipleArg(arg1); | ||
expect(result.arg1.arg).to.equal(1); | ||
}); | ||
it("auto-detect vArg",function() { | ||
const arg1 = 1, arg2 = 2; | ||
expect(varArg.keyValues()).to.equal(null); | ||
expect(Array.isArray(varArg.values())).to.equal(true); | ||
expect(Array.isArray(varArg(arg1,arg2))).to.equal(true); | ||
}); | ||
it("expires content",function(done) { | ||
const expiring = nanomemoize((a) => a,{maxAge:5}); | ||
expect(expiring(1)).to.equal(1); | ||
expect(expiring.keyValues()[1]).to.equal(1); | ||
setTimeout(() => { | ||
expect(expiring.keyValues()[1]).to.equal(undefined); | ||
done(); | ||
},20) | ||
}); | ||
it("clear cache",function() { | ||
const value = 1; | ||
expect(singleArg(value)).to.equal(value); | ||
expect(singleArg.keyValues()[value]).to.equal(value); | ||
singleArg.clear(); | ||
expect(singleArg.keyValues()[value]).to.equal(undefined); | ||
expect(singleArg(value)).to.equal(value); | ||
expect(singleArg.keyValues()[value]).to.equal(value); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
48219
15
941
128
22
2