diff --git a/benchmark/fibonacci.js b/benchmark/fibonacci.js index 9bbab31..d674316 100644 --- a/benchmark/fibonacci.js +++ b/benchmark/fibonacci.js @@ -3,46 +3,111 @@ // Simple benchmark for very simple memoization case (fibonacci series) // To run it, do following in memoizee package path: // -// $ npm install bench underscore lodash lru-cache +// $ npm install underscore lodash lru-cache // $ node benchmark/fibonacci.js -var getFib = function (memoize, opts) { +var forEach = require('es5-ext/lib/Object/for-each') + , pad = require('es5-ext/lib/String/prototype/pad') + , memoizee = require('../lib') + , underscore = require('underscore').memoize + , lodash = require('lodash').memoize + , lruCache = require('lru-cache') + + , now = Date.now + + , time, getFib, lru, memo, total, index = 3000, count = 10, i, lruMax = 1000 + , data = {}, lruObj; + +getFib = function (memoize, opts) { var fib = memoize(function (x) { return (x < 2) ? 1 : fib(x - 1) + fib(x - 2); }, opts); return fib; }; -var memoizee = getFib(require('../lib/memoize'), { primitive: true }) - , underscore = getFib(require('underscore').memoize) - , lodash = getFib(require('lodash').memoize) - - , index = 200; - -var lruCache = require('lru-cache')({}); - -var lru = function (x) { - var value = lruCache.get(x); +lru = function (x) { + var value = lruObj.get(x); if (value === undefined) { value = ((x < 2) ? 1 : lru(x - 1) + lru(x - 2)); - lruCache.set(x, value); + lruObj.set(x, value); } return value; }; -exports.compare = { - "Underscore": function () { - underscore(index); - }, - "Lodash": function () { - lodash(index); - }, - "Memoizee": function () { - memoizee(index); - }, - "LruCache": function () { - lru(index); - } -}; +console.log("Fibonacci", index, "x" + count + ":\n"); -require('bench').runMain(); +total = 0; +i = count; +while (i--) { + memo = getFib(memoizee); + time = now(); + memo(index); + total += now() - time; +} +data["Memoizee (object mode)"] = total; + +total = 0; +i = count; +while (i--) { + memo = getFib(memoizee, { primitive: true }); + time = now(); + memo(index); + total += now() - time; +} +data["Memoizee (primitive mode)"] = total; + +total = 0; +i = count; +while (i--) { + memo = getFib(underscore); + time = now(); + memo(index); + total += now() - time; +} +data["Underscore"] = total; + +total = 0; +i = count; +while (i--) { + memo = getFib(lodash); + time = now(); + memo(index); + total += now() - time; +} +data["Lo-dash"] = total; + +total = 0; +i = count; +while (i--) { + memo = getFib(memoizee, { primitive: true, max: lruMax }); + time = now(); + memo(index); + total += now() - time; +} +data["Memoizee (primitive mode) LRU (max: 1000)"] = total; + +total = 0; +i = count; +while (i--) { + memo = getFib(memoizee, { max: lruMax }); + time = now(); + memo(index); + total += now() - time; +} +data["Memoizee (object mode) LRU (max: 1000)"] = total; + +total = 0; +i = count; +while (i--) { + lruObj = lruCache({ max: lruMax }); + time = now(); + lru(index); + total += now() - time; +} +data["lru-cache LRU (max: 1000)"] = total; + +forEach(data, function (value, name, obj, index) { + console.log(index + 1 + ":", pad.call(value, " ", 5) + "ms ", name); +}, null, function (a, b) { + return this[a] - this[b]; +}); diff --git a/lib/_base.js b/lib/_base.js new file mode 100644 index 0000000..22620ac --- /dev/null +++ b/lib/_base.js @@ -0,0 +1,52 @@ +'use strict'; + +var callable = require('es5-ext/lib/Object/valid-callable') + , forEach = require('es5-ext/lib/Object/for-each') + , ee = require('event-emitter') + + , ext; + +module.exports = exports = function (core) { + return function self(fn/*, options */) { + var options, length, get, clear, conf; + + callable(fn); + options = Object(arguments[1]); + conf = ee({ memoize: self, fn: fn }); + + // Normalize length + if (isNaN(options.length)) { + length = fn.length; + // Special case + if (options.async && ext.async) { + --length; + } + } else { + length = (options.length === false) ? false : (options.length >>> 0); + } + + core(conf, length); + + forEach(ext, function (fn, name) { + if (fn.force) { + fn(conf, options); + } else if (options[name]) { + fn(options[name], conf, options); + } + }); + + fn = conf.fn; + get = conf.get; + clear = conf.clear; + + conf.memoized.clear = function () { clear(get(arguments)); }; + conf.memoized.clearAll = function () { + conf.emit('purgeall'); + conf.clearAll(); + }; + + conf.emit('ready'); + return conf.memoized; + }; +}; +ext = exports.ext = {}; diff --git a/lib/ext/async.js b/lib/ext/async.js new file mode 100644 index 0000000..c2c1050 --- /dev/null +++ b/lib/ext/async.js @@ -0,0 +1,92 @@ +'use strict'; + +var toArray = require('es5-ext/lib/Array/from') + , last = require('es5-ext/lib/Array/prototype/last') + , forEach = require('es5-ext/lib/Object/for-each') + , isCallable = require('es5-ext/lib/Object/is-callable') + , nextTick = require('next-tick') + + , isArray = Array.isArray, slice = Array.prototype.slice + , apply = Function.prototype.apply; + +require('../_base').ext.async = function (ignore, conf, options) { + var cache; + + cache = conf.async = {}; + + (function (org) { + var value, cb; + + conf.on('init', function (id) { + value.id = id; + cache[id] = cb ? [cb] : []; + }); + + conf.on('hit', function (id) { + if (!cb) { + return; + } + + if (isArray(cache[id])) { + cache[id].push(cb); + } else { + nextTick(apply.bind(cb, cache[id].context, cache[id])); + } + }); + conf.fn = function () { + var id, args, asyncArgs; + args = arguments; + asyncArgs = toArray(args); + asyncArgs.push(value = function self(err) { + var i, cb, waiting, res; + if (self.id == null) { + // Shouldn't happen, means async callback was called sync way + nextTick(apply.bind(self, this, arguments)); + return; + } + waiting = cache[self.id]; + if (err) { + conf.clear(self.id); + } else { + arguments.context = this; + cache[self.id] = arguments; + } + for (i = 0; cb = waiting[i]; ++i) { + res = apply.call(cb, this, arguments); + } + return res; + }); + return apply.call(org, this, asyncArgs); + }; + + (function (fn) { + var resolver = function (args) { + cb = last.call(args); + if (isCallable(cb)) { + return slice.call(args, 0, -1); + } else { + cb = null; + return args; + } + }; + conf.memoized = function () { + return fn.apply(this, resolver(arguments)); + }; + forEach(fn, function (value, name) { + memoized[name] = function () { + return fn[name].apply(this, resolver(arguments)); + }; + }); + + }(conf.memoized)); + + }(conf.fn)); + + conf.on('purge', function (id) { + delete cache[id]; + }); + + conf.on('purgeall', function () { + cache = conf.async = {}; + }); +}; diff --git a/lib/ext/dispose.js b/lib/ext/dispose.js new file mode 100644 index 0000000..91ff726 --- /dev/null +++ b/lib/ext/dispose.js @@ -0,0 +1,31 @@ +'use strict'; + +var callable = require('es5-ext/lib/Object/valid-callable') + , forEach = require('es5-ext/lib/Object/for-each') + , ext = require('../_base').ext + + , isArray = Array.isArray, slice = Array.prototype.slice; + +ext.dispose = function (dispose, conf, options) { + var clear; + + callable(dispose); + + conf.on('purge', clear = (options.async && ext.async) ? function (id) { + var value = conf.async[id]; + delete conf.cache[id]; + if (!isArray(value)) { + dispose.apply(null, slice.call(value, 1)); + } + }: function (id) { + var value = conf.cache[id]; + delete conf.cache[id]; + dispose(value); + }); + + conf.on('purgeall', function () { + forEach(conf.cache, function (value, id, cache) { + clear(id); + }); + }); +}; diff --git a/lib/ext/max-age.js b/lib/ext/max-age.js new file mode 100644 index 0000000..412f96d --- /dev/null +++ b/lib/ext/max-age.js @@ -0,0 +1,27 @@ +'use strict'; + +var forEach = require('es5-ext/lib/Object/for-each'); + +require('../_base').ext.maxAge = function (maxAge, conf) { + var cache; + + maxAge = maxAge >>> 0; + if (!maxAge) { + return; + } + + cache = {}; + conf.on('init', function (id) { + cache[id] = setTimeout(function () { conf.clear(id); }, maxAge); + }); + conf.on('purge', function (id) { + clearTimeout(cache[id]); + delete cache[id]; + }); + conf.on('purgeall', function (id) { + forEach(cache, function (id) { + clearTimeout(id); + }); + cache = {}; + }); +}; diff --git a/lib/ext/max.js b/lib/ext/max.js new file mode 100644 index 0000000..77e4937 --- /dev/null +++ b/lib/ext/max.js @@ -0,0 +1,55 @@ +'use strict'; + +require('../_base').ext.max = function (max, conf) { + var index, base, size, add, top, clear, queue, map; + + max = max >>> 0; + if (!max) { + return; + } + + index = -1; + base = size = 0; + queue = {}; + map = {}; + + conf.on('init', function (id) { + queue[++index] = id; + map[id] = index; + ++size; + if (size > max) { + conf.clear(queue[base]); + } + }); + + conf.on('hit', function (id) { + var oldIndex = map[id]; + queue[++index] = queue[oldIndex]; + map[id] = index; + delete queue[oldIndex]; + if (base === oldIndex) { + while (!queue.hasOwnProperty(++base)) continue; + } + }); + + conf.on('purge', function (id) { + var oldIndex = map[id]; + delete queue[oldIndex]; + --size; + if (base === oldIndex) { + if (!size) { + index = -1; + base = 0; + } else { + while (!queue.hasOwnProperty(++base)) continue; + } + } + }); + + conf.on('purgeall', function (id) { + index = -1; + base = size = 0; + queue = {}; + map = {}; + }); +}; diff --git a/lib/ext/method.js b/lib/ext/method.js new file mode 100644 index 0000000..3cf390e --- /dev/null +++ b/lib/ext/method.js @@ -0,0 +1,33 @@ +'use strict'; + +var extend = require('es5-ext/lib/Object/extend') + , isString = require('es5-ext/lib/String/is-string') + + , slice = Array.prototype.slice, create = Object.create + , defineProperty = Object.defineProperty; + +require('../_base').ext.method = function (method, conf, options) { + if (isString(options.method)) { + method = { name: String(options.method), + descriptor: { configurable: true, writable: true } }; + } else { + method = options.method; + method.name = String(method.name); + method.descriptor = (method.descriptor == null) ? + { configurable: true, writable: true } : Object(method.descriptor); + } + options = create(options); + options.method = undefined; + + (function (fn) { + conf.memoized = function () { + if (this && (this !== global)) { + method.descriptor.value = conf.memoize(conf.fn.bind(this), options); + defineProperty(this, method.name, method.descriptor); + return method.descriptor.value.apply(this, arguments); + } + return fn.apply(this, arguments); + }; + extend(conf.memoized, fn); + }(conf.memoized)); +}; diff --git a/lib/profile.js b/lib/ext/profile.js similarity index 63% rename from lib/profile.js rename to lib/ext/profile.js index b0737e6..89cc3ae 100644 --- a/lib/profile.js +++ b/lib/ext/profile.js @@ -4,23 +4,35 @@ var partial = require('es5-ext/lib/Function/prototype/partial') , forEach = require('es5-ext/lib/Object/for-each') , contains = require('es5-ext/lib/String/prototype/contains') , pad = require('es5-ext/lib/String/prototype/pad') - , memoize = require('./memoize') , max = Math.max - , stats = exports.statistics = {}; + , stats = exports.statistics = {}, ext; -memoize._profile = function () { - var id, stack = (new Error()).stack; - id = stack.split('\n')[3].replace(/\n/g, "\\n").trim(); - if (contains.call(id, '/memoizee/lib')) { - id = stack.split('\n')[4].replace(/\n/g, "\\n").trim(); +require('../_base').ext.profile = ext = function (conf) { + var id, stack, data; + stack = (new Error()).stack + if (!stack.split('\n').slice(3).some(function (line) { + if ((line.indexOf('/memoizee/') === -1) && + (line.indexOf('/es5-ext/') === -1) && + (line.indexOf('/next/lib/fs/_memoize-watcher') === -1) && + (line.indexOf(' (native)') === -1)) { + id = line.replace(/\n/g, "\\n").trim(); + return true; + } + })) { + id = 'unknown'; } + if (!stats[id]) { - stats[id] = { initial: 0, cached: 0, time: 0 }; + stats[id] = { initial: 0, cached: 0 }; } - return stats[id]; + data = stats[id]; + + conf.on('init', function (id) { ++data.initial; }); + conf.on('hit', function (id) { ++data.cached; }); }; +ext.force = true; exports.log = function () { var initial, cached, time, ordered, ipad, cpad, ppad, toPrc, tpad, atime, log; @@ -41,7 +53,6 @@ exports.log = function () { forEach(stats, function (data, name) { initial += data.initial; cached += data.cached; - time += data.time; ordered.push([name, data]); }, null, function (a, b) { return (this[b].initial + this[b].cached) - @@ -52,26 +63,18 @@ exports.log = function () { max(String(initial).length, "Init".length)); cpad = partial.call(pad, " ", max(String(cached).length, "Cache".length)); ppad = partial.call(pad, " ", "%Cache".length); - tpad = partial.call(pad, " ", max((String(time.toFixed(3)) + "s").length, - "Avg init time".length)); log += ipad.call("Init") + " " + cpad.call("Cache") + " " + - ppad.call("%Cache") + " " + - tpad.call("Avg init time") + " Source location\n"; + ppad.call("%Cache") + " Source location\n"; log += ipad.call(initial) + " " + cpad.call(cached) + " " + - ppad.call(toPrc(initial, cached)) + " " + - tpad.call((atime = initial ? ((time / initial) / 1000) : 0).toFixed(3) + - "s") + " (all)\n"; + ppad.call(toPrc(initial, cached)) + " (all)\n"; ordered.forEach(function (data) { var name = data[0]; data = data[1]; log += ipad.call(data.initial) + " " + cpad.call(data.cached) + " " + - ppad.call(toPrc(data.initial, data.cached)) + " " + - tpad.call((atime = - data.initial ? ((data.time / data.initial) / 1000) : 0).toFixed(3) + - "s") + " " + name + "\n"; + ppad.call(toPrc(data.initial, data.cached)) + " " + name + "\n"; }); log += "------------------------------------------------------------\n"; return log; diff --git a/lib/ext/ref-counter.js b/lib/ext/ref-counter.js new file mode 100644 index 0000000..110ee97 --- /dev/null +++ b/lib/ext/ref-counter.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../_base').ext.refCounter = function (ignore, conf) { + var cache, get, clear; + + cache = {}; + + conf.on('init', function (id) { cache[id] = 1; }); + conf.on('hit', function (id) { ++cache[id]; }); + conf.on('purge', function (id) { delete cache[id]; }); + conf.on('purgeall', function () { cache = {}; }); + + conf.memoized.clearRef = function () { + var id = conf.get(arguments); + if (cache.hasOwnProperty(id)) { + if (!--cache[id]) { + conf.clear(id) + return true; + } + return false; + } + return null; + }; +}; diff --git a/lib/ext/resolvers.js b/lib/ext/resolvers.js new file mode 100644 index 0000000..ac9533d --- /dev/null +++ b/lib/ext/resolvers.js @@ -0,0 +1,38 @@ +'use strict'; + +var toArray = require('es5-ext/lib/Array/from') + , forEach = require('es5-ext/lib/Object/for-each') + , callable = require('es5-ext/lib/Object/valid-callable') + + , slice = Array.prototype.slice + + , resolve; + +resolve = function (args) { + return this.map(function (r, i) { + return r ? r(args[i]) : args[i]; + }).concat(slice.call(args, this.length)); +}; + +require('../_base').ext.resolvers = function (resolvers, conf) { + var resolver; + + resolver = toArray(resolvers); + resolver.forEach(function (r) { (r == null) || callable(r); }); + resolver = resolve.bind(resolver); + + (function (fn) { + conf.memoized = function () { + var value; + conf.memoized.args = arguments; + value = fn.apply(this, resolver(arguments)); + delete conf.memoized.args; + return value; + }; + forEach(fn, function (value, name) { + conf.memoized[name] = function () { + return fn[name].apply(this, resolver(arguments)); + }; + }); + }(conf.memoized)); +}; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..cf946cc --- /dev/null +++ b/lib/index.js @@ -0,0 +1,20 @@ +'use strict'; + +var regular = require('./regular') + , primitive = require('./primitive') + + , apply = Function.prototype.apply; + +// Order is important! +require('./ext/dispose'); +require('./ext/resolvers'); +require('./ext/async'); +require('./ext/ref-counter'); +require('./ext/method'); +require('./ext/max-age'); +require('./ext/max'); + +module.exports = function (fn/* options */) { + var options = Object(arguments[1]); + return apply.call(options.primitive ? primitive : regular, this, arguments); +}; diff --git a/lib/memoize.js b/lib/memoize.js deleted file mode 100644 index 7b19aba..0000000 --- a/lib/memoize.js +++ /dev/null @@ -1,478 +0,0 @@ -'use strict'; - -var global = require('es5-ext/lib/global') - , toArray = require('es5-ext/lib/Array/from') - , clearArr = require('es5-ext/lib/Array/prototype/clear') - , indexOf = require('es5-ext/lib/Array/prototype/e-index-of') - , last = require('es5-ext/lib/Array/prototype/last') - , remove = require('es5-ext/lib/Array/prototype/remove') - , curry = require('es5-ext/lib/Function/prototype/curry') - , partial = require('es5-ext/lib/Function/prototype/partial') - , callable = require('es5-ext/lib/Object/valid-callable') - , isCallable = require('es5-ext/lib/Object/is-callable') - , isCopy = require('es5-ext/lib/Object/is-copy') - , forEach = require('es5-ext/lib/Object/for-each') - , isString = require('es5-ext/lib/String/is-string') - , nextTick = require('next-tick') - - , slice = Array.prototype.slice, now = Date.now - , apply = Function.prototype.apply - , create = Object.create, defineProperty = Object.defineProperty - - , resolve, memoize; - -resolve = function (args) { - return this.map(function (r, i) { - return r ? r(args[i]) : args[i]; - }).concat(slice.call(args, this.length)); -}; - -// Results are saved internally within array matrix: -// [0] -> Result of calling function with no arguments -// [1] -> Matrix that keeps results when function is called with one argument -// [1][0] -> Array of arguments with which -// function have been called -// [1][1] -> Array of results that matches [1][0] array -// [2] -> Matrix that keeps results when function is called with two arguments -// [2][0] -> Array of first (of two) arguments with which -// function have been called -// [2][1] -> Matrixes that keeps results for two arguments function calls -// Each matrix matches first argument found in [2][0] -// [2][1][x][0] -> Array of second arguments with which -// function have been called. -// [2][1][x][1] -> Array of results that matches [2][1][x][0] -// arguments array -// ...and so on - -memoize = module.exports = function (fn/*, options*/) { - var mfn, options, length, resolver, method, cache, find, save, clear - , value, primitive, serialize, profile, refCounter, async, initAsync - , maxAge, max, queue, dispose, orgDispose, purgePrimitive; - - callable(fn); - if (fn.memoized) { - // Prevent memoization of already memoized function - return fn; - } - - options = Object(arguments[1]); - - async = Boolean(options.async); - if (isNaN(options.length)) { - length = fn.length; - if (async) { - --length; - } - } else if (options.length === false) { - length = false; - } else { - length = Number(options.length); - } - if (options.resolvers) { - resolver = toArray(options.resolvers); - resolver.forEach(function (r) { - (r == null) || callable(r); - }); - resolver = resolve.bind(resolver); - } - if (options.method) { - if (isString(options.method)) { - method = { name: String(options.method), - descriptor: { configurable: true, writable: true } }; - } else { - method = options.method; - method.name = String(method.name); - method.descriptor = (method.descriptor == null) ? - { configurable: true, writable: true } : Object(method.descriptor); - } - options = create(options); - options.method = undefined; - } - primitive = Boolean(options.primitive); - refCounter = Boolean(options.refCounter); - maxAge = options.maxAge >>> 0; - max = options.max >>> 0; - if (options.dispose != null) { - dispose = orgDispose = callable(options.dispose); - if (async) { - dispose = function (value) { - if (value.value) { - apply.call(orgDispose, null, slice.call(value.value, 1)); - } - }; - } else if (refCounter) { - dispose = function (value) { orgDispose(value.value); }; - } - } - - cache = primitive ? {} : []; - - if (memoize._profile) { - profile = memoize._profile(); - } - - if (max) { - queue = []; - } - - find = function (length, args) { - var index = 0, rset = cache, i; - - if (length === 0) { - value = rset[length]; - return rset.hasOwnProperty(length); - } else if ((rset = rset[length])) { - while (index < (length - 1)) { - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - return false; - } - rset = rset[1][i]; - ++index; - } - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - return false; - } - value = rset[1][i]; - return true; - } - return false; - }; - - save = function (length, args, value) { - var index = 0, rset = cache, i; - - if (length === 0) { - rset[length] = value; - } else { - if (!rset[length]) { - rset[length] = [[], []]; - } - rset = rset[length]; - while (index < (length - 1)) { - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - i = rset[0].push(args[index]) - 1; - rset[1].push([[], []]); - } - rset = rset[1][i]; - ++index; - } - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - i = rset[0].push(args[index]) - 1; - } - rset[1][i] = value; - } - }; - - clear = function (length, args) { - var index = 0, rset = cache, i, path = [], value; - - if (length === 0) { - if (dispose && rset.hasOwnProperty("0")) { - value = rset[0]; - delete rset[0]; - dispose(value); - } else { - delete rset[length]; - } - } else if ((rset = rset[length])) { - while (index < (length - 1)) { - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - return; - } - path.push(rset, i); - rset = rset[1][i]; - ++index; - } - i = indexOf.call(rset[0], args[index]); - if (i === -1) { - return; - } - rset[0].splice(i, 1); - if (dispose) { - dispose(rset[1][i]); - } - rset[1].splice(i, 1); - while (!rset[0].length && path.length) { - i = path.pop(); - rset = path.pop(); - rset[0].splice(i, 1); - rset[1].splice(i, 1); - } - } - }; - - if (primitive && (length !== 1)) { - serialize = function (args, length) { - var id = '', i; - if (length) { - id += args[i = 0]; - while (--length) { - id += '\u0001' + args[++i]; - } - } else { - id = '\u0002'; - } - return id; - }; - } - - purgePrimitive = function (id) { - var value; - if (cache.hasOwnProperty(id)) { - value = cache[id]; - delete cache[id]; - if (dispose) { - dispose(value); - } - } - }; - - initAsync = function (args, cb, maxAgeData) { - var waiting = cb ? [cb] : [] - , value = { waiting: waiting }, time; - args.push(function (err) { - var args2 = arguments; - if (profile) { - profile.time += (now() - time); - } - if (!err) { - value.value = arguments; - if (max) { - if (primitive) { - queue.push(maxAgeData); - if (queue.length > max) { - purgePrimitive(queue.shift()); - } - } else { - queue.push([maxAgeData, args]); - if (queue.length > max) { - clear.apply(null, queue.shift()); - } - } - } - if (maxAge) { - setTimeout(primitive ? partial.call(purgePrimitive, maxAgeData) : - partial.call(clear, maxAgeData, args.slice(0, -1)), maxAge); - } - } else { - mfn.clear.apply(this, args.slice(0, -1)); - } - waiting.forEach(function (cb) { - cb.apply(null, args2); - }); - delete value.waiting; - }); - if (profile) { - time = now(); - } - return value; - }; - - mfn = function () { - var args, alength, id, cb, time; - if (method && this && (this !== global)) { - method.descriptor.value = memoize(fn.bind(this), options); - defineProperty(this, method.name, method.descriptor); - return method.descriptor.value.apply(this, arguments); - } - args = arguments; - if (async) { - cb = last.call(args); - if (isCallable(cb)) { - args = slice.call(args, 0, -1); - } else { - cb = null; - } - } - if (resolver) { - args = resolver(args); - } - alength = (length === false) ? args.length : length; - - if (primitive) { - id = (length === 1) ? args[0] : serialize(args, alength); - if (cache.hasOwnProperty(id)) { - profile && ++profile.cached; - value = cache[id]; - } else { - profile && ++profile.initial; - mfn.args = arguments; - mfn.preventCache = false; - if (async) { - args = toArray(args); - value = initAsync(args, cb, id); - value.sync = apply.call(fn, this, args); - } else { - if (profile) { - time = now(); - } - value = apply.call(fn, this, args); - if (profile) { - profile.time += (now() - time); - } - } - if (!mfn.preventCache) { - if (refCounter) { - if (async) { - value.count = cb ? 1 : 0; - } else { - value = { value: value, count: 1 }; - } - } - cache[id] = value; - if (max && !async) { - queue.push(id); - if (queue.length > max) { - purgePrimitive(queue.shift()); - } - } - if (maxAge && !async) { - setTimeout(partial.call(purgePrimitive, id), maxAge); - } - } - delete mfn.args; - return async ? value.sync : (refCounter ? value.value : value); - } - } else if (find(alength, args)) { - profile && ++profile.cached; - } else { - profile && ++profile.initial; - mfn.args = arguments; - mfn.preventCache = false; - if (async) { - args = toArray(args); - value = initAsync(args, cb, alength); - value.sync = apply.call(fn, this, args); - } else { - if (profile) { - time = now(); - } - value = apply.call(fn, this, args); - if (profile) { - profile.time += (now() - time); - } - } - if (!mfn.preventCache) { - if (refCounter) { - if (async) { - value.count = cb ? 1 : 0; - } else { - value = { value: value, count: 1 }; - } - } - save(alength, args, value); - if (max && !async) { - queue.push([alength, args]); - if (queue.length > max) { - clear.apply(null, queue.shift()); - } - } - if (maxAge && !async) { - setTimeout(partial.call(clear, alength, args), maxAge); - } - } - delete mfn.args; - return async ? value.sync : (refCounter ? value.value : value); - } - - if (async) { - if (cb) { - if (refCounter) { - ++value.count; - } - if (value.value) { - nextTick(function () { - cb.apply(null, this); - }.bind(value.value)); - } else { - value.waiting.push(cb); - } - } - return value.sync; - } else { - if (refCounter) { - ++value.count; - value = value.value; - } - return value; - } - }; - mfn.memoized = true; - - mfn.clear = function () { - var args, alength, id; - args = resolver ? resolver(arguments) : arguments; - alength = (length === false) ? args.length : length; - - if (primitive) { - id = (length === 1) ? args[0] : serialize(args, alength); - if (max) { - remove.call(queue, id); - } - purgePrimitive(id); - } else { - if (max) { - if (find.call(queue, function (data, index) { - if (isCopy(data, [alength, args], 2)) { - id = index; - return true; - } - })) { - queue.splice(id, 1); - } - } - clear(alength, args); - } - }; - - mfn.clearAll = function () { - if (dispose) { - if (primitive) { - forEach(cache, curry.call(dispose, 1)); - } else { - cache.forEach(function self(value, index) { - if (!index) { - dispose(value); - return; - } - --index; - value[1].forEach(function (value) { self(value, index); }); - }); - } - } - if (max) { - clearArr.call(queue); - } - cache = primitive ? {} : []; - }; - - if (refCounter) { - mfn.clearRef = function () { - var args, alength, id; - args = resolver ? resolver(arguments) : arguments; - alength = (length === false) ? args.length : length; - - if (primitive) { - id = (length === 1) ? args[0] : serialize(args, alength); - if (!cache.hasOwnProperty(id)) { - return null; - } - value = cache[id]; - } else if (!find(alength, args)) { - return null; - } - if (!--value.count) { - mfn.clear.apply(null, arguments); - return true; - } - return false; - }; - } - - return mfn; -}; diff --git a/lib/primitive.js b/lib/primitive.js new file mode 100644 index 0000000..43b75ea --- /dev/null +++ b/lib/primitive.js @@ -0,0 +1,81 @@ +'use strict'; + +var hasListeners = require('event-emitter/lib/has-listeners') + + , getId0 = function () { return ''; } + , getId1 = function (args) { return args[0]; } + + , apply = Function.prototype.apply, call = Function.prototype.call; + +module.exports = require('./_base')(function (conf, length) { + var get, cache = conf.cache = {}, fn + , hitListeners, initListeners, purgeListeners; + + if (length === 1) { + get = conf.get = getId1; + } else if (length === false) { + get = conf.get = function (args, length) { + var id = '', i; + if (length) { + id += args[i = 0]; + while (--length) { + id += '\u0001' + args[++i]; + } + } else { + id = '\u0002'; + } + return id; + }; + } else if (length) { + get = conf.get = function(args) { + var id = String(args[0]), i = 0, l = length; + while (--l) { id += '\u0001' + args[++i]; } + return id; + }; + } else { + get = conf.get = getId0; + } + + conf.memoized = (length === 1) ? function (id) { + var value; + if (cache.hasOwnProperty(id)) { + hitListeners && conf.emit('hit', id); + return cache[id]; + } else { + if (arguments.length === 1) { + value = call.call(fn, this, id); + } else { + value = apply.call(fn, this, arguments); + } + cache[id] = value; + initListeners && conf.emit('init', id); + return value; + } + } : function () { + var id = get(arguments), value; + if (cache.hasOwnProperty(id)) { + hitListeners && conf.emit('hit', id); + return cache[id]; + } else { + value = apply.call(conf.fn, this, arguments); + cache[id] = value; + initListeners && conf.emit('init', id); + return value; + } + }; + + conf.clear = function (id) { + if (cache.hasOwnProperty(id)) { + purgeListeners && conf.emit('purge', id); + delete cache[id]; + } + }; + conf.clearAll = function () { cache = conf.cache = {}; }; + + conf.once('ready', function () { + fn = conf.fn; + hitListeners = hasListeners(conf, 'hit'); + initListeners = hasListeners(conf, 'init'); + purgeListeners = hasListeners(conf, 'purge'); + }); +}); diff --git a/lib/regular.js b/lib/regular.js new file mode 100644 index 0000000..17bb538 --- /dev/null +++ b/lib/regular.js @@ -0,0 +1,240 @@ +'use strict'; + +var indexOf = require('es5-ext/lib/Array/prototype/e-index-of') + , hasListeners = require('event-emitter/lib/has-listeners') + + , apply = Function.prototype.apply + + , getId0 = function () { return ''; }; + +// Results are saved internally within array matrix: +// [0] -> Result of calling function with no arguments +// [1] -> Matrix that keeps results when function is called with one argument +// [1][0] -> Array of arguments with which +// function have been called +// [1][1] -> Array of results that matches [1][0] array +// [2] -> Matrix that keeps results when function is called with two arguments +// [2][0] -> Array of first (of two) arguments with which +// function have been called +// [2][1] -> Matrixes that keeps results for two arguments function calls +// Each matrix matches first argument found in [2][0] +// [2][1][x][0] -> Array of second arguments with which +// function have been called. +// [2][1][x][1] -> Array of results that matches [2][1][x][0] +// arguments array +// ...and so on +module.exports = require('./_base')(function (conf, length) { + var map, map1, map2, get, set, clear, count, fn + , hitListeners, initListeners, purgeListeners + , cache = conf.cache = {}; + + if (length === 0) { + get = set = clear = conf.get = getId0; + conf.clearAll = function () { cache = conf.cache = {}; }; + } else { + count = 0; + if (length === 1) { + map1 = []; map2 = []; + get = conf.get = function (args) { + return map2[indexOf.call(map1, args[0])]; + }; + set = function (args) { + map1.push(args[0]); + map2.push(++count); + return count; + }; + clear = function (args) { + var index = indexOf.call(map1, args[0]), id; + if (index !== -1) { + id = map2[index]; + map1.splice(index, 1); + map2.splice(index, 1); + return id; + } + }; + conf.clearAll = function () { + map1 = []; + map2 = []; + cache = conf.cache = {}; + }; + } else if (length === false) { + map = []; + get = conf.get = function (args) { + var index = 0, set = map, i, length = args.length; + if (length === 0) { + return set[length]; + } else if ((set = set[length])) { + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + return set[1][i]; + } + return; + }; + set = function (args) { + var index = 0, set = map, i, length = args.length; + if (length === 0) { + set[length] = ++count; + } else { + if (!set[length]) { + set[length] = [[], []]; + } + set = set[length]; + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + i = set[0].push(args[index]) - 1; + set[1].push([[], []]); + } + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + i = set[0].push(args[index]) - 1; + } + set[1][i] = ++count; + } + return count; + }; + clear = function (args) { + var index = 0, set = map, i, length = args.length, path =[], id; + if (length === 0) { + id = set[length]; + delete set[length]; + return id; + } else if ((set = set[length])) { + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + path.push(set, i); + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + id = set[1][i]; + set[0].splice(i, 1); + set[1].splice(i, 1); + while (!set[0].length && path.length) { + i = path.pop(); + set = path.pop(); + set[0].splice(i, 1); + set[1].splice(i, 1); + } + return id; + } + }; + conf.clearAll = function () { + map = []; + cache = conf.cache = {}; + }; + } else { + map = [[], []]; + get = conf.get = function (args) { + var index = 0, set = map, i; + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return null; + } + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return null; + } + return set[1][i]; + }; + set = function (args) { + var index = 0, set = map, i; + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + i = set[0].push(args[index]) - 1; + set[1].push([[], []]); + } + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + i = set[0].push(args[index]) - 1; + } + set[1][i] = ++count; + return count; + }; + clear = function (args) { + var index = 0, set = map, i, path =[], id; + while (index < (length - 1)) { + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + path.push(set, i); + set = set[1][i]; + ++index; + } + i = indexOf.call(set[0], args[index]); + if (i === -1) { + return; + } + id = set[1][i]; + set[0].splice(i, 1); + set[1].splice(i, 1); + while (!set[0].length && path.length) { + i = path.pop(); + set = path.pop(); + set[0].splice(i, 1); + set[1].splice(i, 1); + } + return id; + }; + conf.clearAll = function () { + map = [[], []]; + cache = conf.cache = {}; + }; + } + } + conf.memoized = function () { + var id = get(arguments), value; + if (cache.hasOwnProperty(id)) { + hitListeners && conf.emit('hit', id); + return cache[id]; + } else { + value = apply.call(fn, this, arguments); + id = set(arguments); + cache[id] = value; + initListeners && conf.emit('init', id); + return value; + } + }; + conf.clear = function (id) { + if (cache.hasOwnProperty(id)) { + clear(id); + purgeListeners && conf.emit('purge', id); + delete cache[id]; + } + }; + + conf.once('ready', function () { + fn = conf.fn; + hitListeners = hasListeners(conf, 'hit'); + initListeners = hasListeners(conf, 'init'); + purgeListeners = hasListeners(conf, 'purge'); + }); +}); diff --git a/package.json b/package.json index badcdb8..484a6d5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "memoizee", "version": "0.1.1", "description": "Complete memoize/cache solution. Works with any type and length of function arguments", - "main": "lib/memoize", + "main": "lib", "scripts": { "test": "node node_modules/tad/bin/tad lib" }, @@ -35,6 +35,7 @@ }, "dependencies": { "es5-ext": "0.9.x", + "event-emitter": "~0.2.1", "next-tick": "0.1.x" }, "devDependencies": { diff --git a/test/_base.js b/test/_base.js new file mode 100644 index 0000000..2d2a6d5 --- /dev/null +++ b/test/_base.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = function (t, a) { + var mfn, m = t(function (conf, length) { + conf.get = function () { return 'test'; }; + conf.memoized = function (x, y, z) { return conf.fn(z, y, x); }; + conf.clear = function () {}; + conf.clearAll = function () {}; + }); + + a(typeof m, 'function', "Returns"); + mfn = m(function (x, y, z) { return [x, y, z] }); + a(typeof mfn, 'function', "Generates"); + a.deep(mfn(3, 7, 11), [11, 7, 3], "Works"); +}; diff --git a/test/ext/async.js b/test/ext/async.js new file mode 100644 index 0000000..c1f59c8 --- /dev/null +++ b/test/ext/async.js @@ -0,0 +1,351 @@ +'use strict'; + +var memoize = require('../../lib') + , nextTick = require('next-tick'); + +module.exports = function (a) { + return { + "Regular": { + "Success": function (a, d) { + var mfn, fn, u = {}, i = 0, invoked = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true }); + + a(mfn(3, 7, function (err, res) { + ++invoked; + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + ++invoked; + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + ++invoked; + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + ++invoked; + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + ++invoked; + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Init Called"); + a(invoked, 5, "Cb Called"); + + a(mfn(3, 7, function (err, res) { + ++invoked; + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + ++invoked; + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 2, "Init Called #2"); + a(invoked, 7, "Cb Called #2"); + + mfn.clear(3, 7); + + a(mfn(3, 7, function (err, res) { + ++invoked; + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + ++invoked; + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 3, "Init After clear"); + a(invoked, 9, "Cb After clear"); + d(); + }); + }); + }); + }, + "Reference counter": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, refCounter: true }); + + a(mfn.clearRef(3, 7), null, "Clear ref before"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 2, "Again Called #2"); + + a(mfn.clearRef(3, 7), false, "Clear ref #1"); + a(mfn.clearRef(3, 7), false, "Clear ref #2"); + a(mfn.clearRef(3, 7), false, "Clear ref #3"); + a(mfn.clearRef(3, 7), true, "Clear ref Final"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 3, "Call After clear"); + d(); + }); + }); + }); + }, + "Error": function (a, d) { + var mfn, fn, u = {}, i = 0, e = new Error("Test"); + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(e); + }); + return u; + }; + + mfn = memoize(fn, { async: true }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 4, "Again Called #2"); + d(); + }); + }); + } + }, + "Primitive": { + "Success": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, primitive: true }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 2, "Again Called #2"); + + mfn.clear(3, 7); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 3, "Call After clear"); + d(); + }); + }); + }); + }, + "Reference counter": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, primitive: true, refCounter: true }); + + a(mfn.clearRef(3, 7), null, "Clear ref before"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 2, "Again Called #2"); + + a(mfn.clearRef(3, 7), false, "Clear ref #1"); + a(mfn.clearRef(3, 7), false, "Clear ref #2"); + a(mfn.clearRef(3, 7), false, "Clear ref #3"); + a(mfn.clearRef(3, 7), true, "Clear ref Final"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 3, "Call After clear"); + d(); + }); + }); + }); + }, + "Error": function (a, d) { + var mfn, fn, u = {}, i = 0, e = new Error("Test"); + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(e); + }); + return u; + }; + + mfn = memoize(fn, { async: true, primitive: true }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Result B #2"); + }), u, "Initial #3"); + + nextTick(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [e, undefined], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [e, undefined], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 4, "Again Called #2"); + d(); + }); + }); + } + } + }; +}; \ No newline at end of file diff --git a/test/ext/dispose.js b/test/ext/dispose.js new file mode 100644 index 0000000..e547a3d --- /dev/null +++ b/test/ext/dispose.js @@ -0,0 +1,191 @@ +'use strict'; + +var memoize = require('../../lib') + , nextTick = require('next-tick'); + +module.exports = function (a) { + return { + "Regular": { + "Sync": function (a) { + var mfn, fn, i = 0, value = [], x, invoked; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { dispose: function (val) { value.push(val); } }); + + mfn(3, 7); + mfn(5, 8); + mfn(12, 4); + a.deep(value, [], "Pre"); + mfn.clear(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clear(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11); + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + + x = {}; + invoked = false; + mfn = memoize(function () { return x; }, + { dispose: function (val) { invoked = val; } }); + + mfn.clear(); + a(invoked, false, "No args: Post invalid clear"); + mfn(); + a(invoked, false, "No args: Post cache"); + mfn.clear(); + a(invoked, x, "No args: Pre clear"); + }, + "Ref counter": function (a) { + var mfn, fn, i = 0, value = []; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { refCounter: true, + dispose: function (val) { value.push(val); } }); + + mfn(3, 7); + mfn(5, 8); + mfn(12, 4); + a.deep(value, [], "Pre"); + mfn(5, 8); + mfn.clearRef(5, 8); + a.deep(value, [], "Pre"); + mfn.clearRef(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clearRef(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11); + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0, value = []; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, + dispose: function (val) { value.push(val); } }); + + mfn(3, 7, function () { + mfn(5, 8, function () { + mfn(12, 4, function () { + a.deep(value, [], "Pre"); + mfn.clear(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clear(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11, function () { + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + d(); + }); + }); + }); + }); + } + }, + "Primitive": { + "Sync": function (a) { + var mfn, fn, i = 0, value = []; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { dispose: function (val) { value.push(val); } }); + + mfn(3, 7); + mfn(5, 8); + mfn(12, 4); + a.deep(value, [], "Pre"); + mfn.clear(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clear(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11); + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + }, + "Ref counter": function (a) { + var mfn, fn, i = 0, value = []; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { refCounter: true, + dispose: function (val) { value.push(val); } }); + + mfn(3, 7); + mfn(5, 8); + mfn(12, 4); + a.deep(value, [], "Pre"); + mfn(5, 8); + mfn.clearRef(5, 8); + a.deep(value, [], "Pre"); + mfn.clearRef(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clearRef(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11); + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0, value = []; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, + dispose: function (val) { value.push(val); } }); + + mfn(3, 7, function () { + mfn(5, 8, function () { + mfn(12, 4, function () { + a.deep(value, [], "Pre"); + mfn.clear(5, 8); + a.deep(value, [13], "#1"); + value = []; + mfn.clear(12, 4); + a.deep(value, [16], "#2"); + + value = []; + mfn(77, 11, function () { + mfn.clearAll(); + a.deep(value, [10, 88], "Clear all"); + d(); + }); + }); + }); + }); + } + } + }; +}; diff --git a/test/ext/max-age.js b/test/ext/max-age.js new file mode 100644 index 0000000..c7b1321 --- /dev/null +++ b/test/ext/max-age.js @@ -0,0 +1,201 @@ +'use strict'; + +var memoize = require('../../lib') + , nextTick = require('next-tick'); + +module.exports = function (a) { + return { + "Regular": { + "Sync": function (a, d) { + var mfn, fn, i = 0; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { maxAge: 100 }); + + a(mfn(3, 7), 10, "Result #1"); + a(i, 1, "Called #1"); + a(mfn(3, 7), 10, "Result #2"); + a(i, 1, "Called #2"); + a(mfn(5, 8), 13, "Result B #1"); + a(i, 2, "Called B #1"); + a(mfn(3, 7), 10, "Result #3"); + a(i, 2, "Called #3"); + a(mfn(5, 8), 13, "Result B #2"); + a(i, 2, "Called B #2"); + + setTimeout(function () { + a(mfn(3, 7), 10, "Result: Wait"); + a(i, 2, "Called: Wait"); + a(mfn(5, 8), 13, "Result: Wait B"); + a(i, 2, "Called: Wait B"); + + setTimeout(function () { + a(mfn(3, 7), 10, "Result: Wait After"); + a(i, 3, "Called: Wait After"); + a(mfn(5, 8), 13, "Result: Wait After B"); + a(i, 4, "Called: Wait After B"); + + a(mfn(3, 7), 10, "Result: Wait After #2"); + a(i, 4, "Called: Wait After #2"); + a(mfn(5, 8), 13, "Result: Wait After B #2"); + a(i, 4, "Called: Wait After B #2"); + d(); + }, 100); + }, 20); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, maxAge: 100 }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + setTimeout(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + setTimeout(function () { + a(i, 2, "Again Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 4, "Call After clear"); + d(); + }); + }, 100); + }, 20); + } + }, + "Primitive": { + "Sync": function (a, d) { + var mfn, fn, i = 0; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { primitive: true, maxAge: 100 }); + + a(mfn(3, 7), 10, "Result #1"); + a(i, 1, "Called #1"); + a(mfn(3, 7), 10, "Result #2"); + a(i, 1, "Called #2"); + a(mfn(5, 8), 13, "Result B #1"); + a(i, 2, "Called B #1"); + a(mfn(3, 7), 10, "Result #3"); + a(i, 2, "Called #3"); + a(mfn(5, 8), 13, "Result B #2"); + a(i, 2, "Called B #2"); + + setTimeout(function () { + a(mfn(3, 7), 10, "Result: Wait"); + a(i, 2, "Called: Wait"); + a(mfn(5, 8), 13, "Result: Wait B"); + a(i, 2, "Called: Wait B"); + + setTimeout(function () { + a(mfn(3, 7), 10, "Result: Wait After"); + a(i, 3, "Called: Wait After"); + a(mfn(5, 8), 13, "Result: Wait After B"); + a(i, 4, "Called: Wait After B"); + + a(mfn(3, 7), 10, "Result: Wait After #2"); + a(i, 4, "Called: Wait After #2"); + a(mfn(5, 8), 13, "Result: Wait After B #2"); + a(i, 4, "Called: Wait After B #2"); + d(); + }, 100); + }, 20); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, primitive: true, maxAge: 100 }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + }), u, "Initial"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + }), u, "Initial #2"); + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + }), u, "Initial #2"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + }), u, "Initial #3"); + + setTimeout(function () { + a(i, 2, "Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + setTimeout(function () { + a(i, 2, "Again Called #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Again: Result"); + }), u, "Again: Initial"); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Again B: Result"); + }), u, "Again B: Initial"); + + nextTick(function () { + a(i, 4, "Call After clear"); + d(); + }); + }, 100); + }, 20); + } + } + }; +}; diff --git a/test/ext/max.js b/test/ext/max.js new file mode 100644 index 0000000..21be113 --- /dev/null +++ b/test/ext/max.js @@ -0,0 +1,361 @@ +'use strict'; + +var memoize = require('../../lib') + , nextTick = require('next-tick'); + +module.exports = function (a) { + return { + "Regular": { + "Sync": function (a) { + var mfn, fn, i = 0; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { max: 3 }); + + a(mfn(3, 7), 10, "Result #1"); + a(i, 1, "Called #1"); + a(mfn(3, 7), 10, "Result #2"); + a(i, 1, "Called #2"); + a(mfn(5, 8), 13, "Result B #1"); + a(i, 2, "Called B #1"); + a(mfn(3, 7), 10, "Result #3"); + a(i, 2, "Called #3"); + a(mfn(5, 8), 13, "Result B #2"); + a(i, 2, "Called B #2"); + a(mfn(12, 4), 16, "Result C #1"); + a(i, 3, "Called C #1"); + a(mfn(3, 7), 10, "Result #4"); + a(i, 3, "Called #4"); + a(mfn(5, 8), 13, "Result B #3"); + a(i, 3, "Called B #3"); + + a(mfn(77, 11), 88, "Result D #1"); // Clear 12, 4 + a(i, 4, "Called D #1"); + a(mfn(5, 8), 13, "Result B #4"); + a(i, 4, "Called B #4"); + a(mfn(12, 4), 16, "Result C #2"); // Clear 3, 7 + a(i, 5, "Called C #2"); + + a(mfn(3, 7), 10, "Result #5"); // Clear 77, 11 + a(i, 6, "Called #5"); + a(mfn(77, 11), 88, "Result D #2"); // Clear 5, 8 + a(i, 7, "Called D #2"); + a(mfn(12, 4), 16, "Result C #3"); + a(i, 7, "Called C #3"); + + a(mfn(5, 8), 13, "Result B #5"); // Clear 3, 7 + a(i, 8, "Called B #5"); + + a(mfn(77, 11), 88, "Result D #3"); + a(i, 8, "Called D #3"); + + mfn.clear(77, 11); + a(mfn(77, 11), 88, "Result D #4"); + a(i, 9, "Called D #4"); + + mfn.clearAll(); + a(mfn(5, 8), 13, "Result B #6"); + a(i, 10, "Called B #6"); + a(mfn(77, 11), 88, "Result D #5"); + a(i, 11, "Called D #5"); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, max: 3 }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + a(i, 1, "Called #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + a(i, 1, "Called #2"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + a(i, 2, "Called B #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + a(i, 2, "Called #3"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + a(i, 2, "Called B #2"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], "Result C #1"); + a(i, 3, "Called C #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #4"); + a(i, 3, "Called #4"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #3"); + a(i, 3, "Called B #3"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], "Result D #1"); + a(i, 4, "Called D #1"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #4"); + a(i, 4, "Called B #4"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], "Result C #2"); + a(i, 5, "Called C #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #5"); + a(i, 6, "Called #5"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #2"); + a(i, 7, "Called D #2"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], + "Result C #3"); + a(i, 7, "Called C #3"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], + "Result B #5"); + a(i, 8, "Called B #5"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #3"); + a(i, 8, "Called D #3"); + + mfn.clear(77, 11); + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #4"); + a(i, 9, "Called D #4"); + + mfn.clearAll(); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], + "Result B #6"); + a(i, 10, "Called B #6"); + + a(mfn(77, 11, + function (err, res) { + a.deep([err, res], [null, 88], + "Result D #5"); + a(i, 11, "Called D #5"); + + d(); + }), u, "Initial D #5"); + }), u, "Initial B #6"); + }), u, "Initial D #4"); + }), u, "Initial D #3"); + }), u, "Initial B #5"); + }), u, "Initial C #3"); + }), u, "Initial D #2"); + }), u, "Initial #5"); + }), u, "Initial C #2"); + }), u, "Initial B #4"); + }), u, "Initial D #1"); + }), u, "Initial B #3"); + }), u, "Initial #4"); + }), u, "Initial C #1"); + }), u, "Initial B #2"); + }), u, "Initial #3"); + }), u, "Initial B #1"); + }), u, "Initial #2"); + }), u, "Initial #1"); + } + }, + "Primitive": { + "Sync": function (a) { + var mfn, fn, i = 0; + fn = function (x, y) { + ++i; + return x + y; + }; + mfn = memoize(fn, { primitive: true, max: 3 }); + + a(mfn(3, 7), 10, "Result #1"); + a(i, 1, "Called #1"); + a(mfn(3, 7), 10, "Result #2"); + a(i, 1, "Called #2"); + a(mfn(5, 8), 13, "Result B #1"); + a(i, 2, "Called B #1"); + a(mfn(3, 7), 10, "Result #3"); + a(i, 2, "Called #3"); + a(mfn(5, 8), 13, "Result B #2"); + a(i, 2, "Called B #2"); + a(mfn(12, 4), 16, "Result C #1"); + a(i, 3, "Called C #1"); + a(mfn(3, 7), 10, "Result #4"); + a(i, 3, "Called #4"); + a(mfn(5, 8), 13, "Result B #3"); + a(i, 3, "Called B #3"); + + a(mfn(77, 11), 88, "Result D #1"); // Clear 12, 4 + a(i, 4, "Called D #1"); + a(mfn(5, 8), 13, "Result B #4"); + a(i, 4, "Called B #4"); + a(mfn(12, 4), 16, "Result C #2"); // Clear 3, 7 + a(i, 5, "Called C #2"); + + a(mfn(3, 7), 10, "Result #5"); // Clear 77, 11 + a(i, 6, "Called #5"); + a(mfn(77, 11), 88, "Result D #2"); // Clear 5, 8 + a(i, 7, "Called D #2"); + a(mfn(12, 4), 16, "Result C #3"); + a(i, 7, "Called C #3"); + + a(mfn(5, 8), 13, "Result B #5"); // Clear 3, 7 + a(i, 8, "Called B #5"); + + a(mfn(77, 11), 88, "Result D #3"); + a(i, 8, "Called D #3"); + + mfn.clear(77, 11); + a(mfn(77, 11), 88, "Result D #4"); + a(i, 9, "Called D #4"); + + mfn.clearAll(); + a(mfn(5, 8), 13, "Result B #6"); + a(i, 10, "Called B #6"); + a(mfn(77, 11), 88, "Result D #5"); + a(i, 11, "Called D #5"); + }, + "Async": function (a, d) { + var mfn, fn, u = {}, i = 0; + fn = function (x, y, cb) { + nextTick(function () { + ++i; + cb(null, x + y); + }); + return u; + }; + + mfn = memoize(fn, { async: true, primitive: true, max: 3 }); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #1"); + a(i, 1, "Called #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #2"); + a(i, 1, "Called #2"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #1"); + a(i, 2, "Called B #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #3"); + a(i, 2, "Called #3"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #2"); + a(i, 2, "Called B #2"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], "Result C #1"); + a(i, 3, "Called C #1"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #4"); + a(i, 3, "Called #4"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #3"); + a(i, 3, "Called B #3"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], "Result D #1"); + a(i, 4, "Called D #1"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], "Result B #4"); + a(i, 4, "Called B #4"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], "Result C #2"); + a(i, 5, "Called C #2"); + + a(mfn(3, 7, function (err, res) { + a.deep([err, res], [null, 10], "Result #5"); + a(i, 6, "Called #5"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #2"); + a(i, 7, "Called D #2"); + + a(mfn(12, 4, function (err, res) { + a.deep([err, res], [null, 16], + "Result C #3"); + a(i, 7, "Called C #3"); + + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], + "Result B #5"); + a(i, 8, "Called B #5"); + + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #3"); + a(i, 8, "Called D #3"); + + mfn.clear(77, 11); + a(mfn(77, 11, function (err, res) { + a.deep([err, res], [null, 88], + "Result D #4"); + a(i, 9, "Called D #4"); + + mfn.clearAll(); + a(mfn(5, 8, function (err, res) { + a.deep([err, res], [null, 13], + "Result B #6"); + a(i, 10, "Called B #6"); + + a(mfn(77, 11, + function (err, res) { + a.deep([err, res], [null, 88], + "Result D #5"); + a(i, 11, "Called D #5"); + + d(); + }), u, "Initial D #5"); + }), u, "Initial B #6"); + }), u, "Initial D #4"); + }), u, "Initial D #3"); + }), u, "Initial B #5"); + }), u, "Initial C #3"); + }), u, "Initial D #2"); + }), u, "Initial #5"); + }), u, "Initial C #2"); + }), u, "Initial B #4"); + }), u, "Initial D #1"); + }), u, "Initial B #3"); + }), u, "Initial #4"); + }), u, "Initial C #1"); + }), u, "Initial B #2"); + }), u, "Initial #3"); + }), u, "Initial B #1"); + }), u, "Initial #2"); + }), u, "Initial #1"); + } + } + }; +}; diff --git a/test/ext/method.js b/test/ext/method.js new file mode 100644 index 0000000..f69f25a --- /dev/null +++ b/test/ext/method.js @@ -0,0 +1,34 @@ +'use strict'; + +var memoize = require('../../lib'); + +module.exports = function (a) { + return { + "No descriptor": function (a) { + var mfn, x = {}, i = 0, fn = function () { + ++i; + return this; + }; + + mfn = memoize(fn, { method: 'foo' }); + a(mfn.call(x), x, "Context"); + a(x.foo(), x, "Method"); + a(i, 1, "Cached"); + }, + "Descriptor": function (a) { + var mfn, x = {}, i = 0, fn = function () { + ++i; + return this; + }; + + mfn = memoize(fn, + { method: { name: 'foo', descriptor: { configurable: true } } }); + a(mfn.call(x), x, "Context"); + a.deep(Object.getOwnPropertyDescriptor(x, 'foo'), + { enumerable: false, configurable: true, writable: false, + value: x.foo }); + a(x.foo(), x, "Method"); + a(i, 1, "Cached"); + } + }; +}; diff --git a/test/profile.js b/test/ext/profile.js similarity index 62% rename from test/profile.js rename to test/ext/profile.js index 5be0c45..78896e5 100644 --- a/test/profile.js +++ b/test/ext/profile.js @@ -1,9 +1,9 @@ 'use strict'; -var memoize = require('../lib/memoize'); +var memoize = require('../../lib'); module.exports = function (t, a) { - a(typeof memoize._profile, 'function', "Set on memoize"); a(typeof t.statistics, 'object', "Access to statistics"); a(typeof t.log, 'function', "Access to log function"); + a(typeof t.log(), 'string', "Log outputs string"); }; diff --git a/test/ext/ref-counter.js b/test/ext/ref-counter.js new file mode 100644 index 0000000..61de708 --- /dev/null +++ b/test/ext/ref-counter.js @@ -0,0 +1,48 @@ +'use strict'; + +var memoize = require('../../lib') + +module.exports = function (a) { + return { + "Regular": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn; + mfn = memoize(fn, { refCounter: true }); + a(mfn.clearRef(3, 5, 7), null, "Clear before"); + a(mfn(3, 5, 7), 15, "Initial"); + a(mfn(3, 5, 7), 15, "Cache"); + a(mfn.clearRef(3, 5, 7), false, "Clear #1"); + mfn(3, 5, 7); + a(mfn.clearRef(3, 5, 7), false, "Clear #2"); + mfn(3, 5, 7); + a(mfn.clearRef(3, 5, 7), false, "Clear #3"); + mfn(3, 5, 7); + a(i, 1, "Not cleared"); + a(mfn.clearRef(3, 5, 7), false, "Clear #4"); + a(mfn.clearRef(3, 5, 7), true, "Clear final"); + mfn(3, 5, 7); + a(i, 2, "Restarted"); + mfn(3, 5, 7); + a(i, 2, "Cached again"); + }, + "Primitive": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn; + mfn = memoize(fn, { primitive: true, refCounter: true }); + a(mfn.clearRef(3, 5, 7), null, "Clear before"); + a(mfn(3, 5, 7), 15, "Initial"); + a(mfn(3, 5, 7), 15, "Cache"); + a(mfn.clearRef(3, 5, 7), false, "Clear #1"); + mfn(3, 5, 7); + a(mfn.clearRef(3, 5, 7), false, "Clear #2"); + mfn(3, 5, 7); + a(mfn.clearRef(3, 5, 7), false, "Clear #3"); + mfn(3, 5, 7); + a(i, 1, "Not cleared"); + a(mfn.clearRef(3, 5, 7), false, "Clear #4"); + a(mfn.clearRef(3, 5, 7), true, "Clear final"); + mfn(3, 5, 7); + a(i, 2, "Restarted"); + mfn(3, 5, 7); + a(i, 2, "Cached again"); + } + }; +}; diff --git a/test/ext/resolvers.js b/test/ext/resolvers.js new file mode 100644 index 0000000..f370109 --- /dev/null +++ b/test/ext/resolvers.js @@ -0,0 +1,47 @@ +'use strict'; + +var toArray = require('es5-ext/lib/Array/from') + , memoize = require('../../lib'); + +module.exports = function (a) { + return { + "Original arguments": function (a) { + var fn, mfn, x = {}; + fn = function (x, y) { x = y; return toArray(mfn.args); }; + mfn = memoize(fn, { resolvers: [] }); + + a.deep(mfn(23, 'raz', x), [23, 'raz', x]); + }, + "Resolvers": function () { + var i = 0, fn, r; + fn = memoize(function () { ++i; return arguments; }, + { length: 3, resolvers: [Boolean, String] }); + return { + "No args": function () { + i = 0; + a.deep(toArray(r = fn()), [false, 'undefined'], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(0, 34, x, 45)), [false, '34', x, 45], + "First"); + a(fn(0, 34, x, 22), r, "Second"); + a(fn(0, 34, x, false), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(toArray(r = fn(1, 34, x, 34)), + [true, '34', x, 34], "Second"); + a(fn(1, 34, x, 89), r, "Third"); + a(i, 2, "Called once"); + } + }; + } + }; + } + }; +}; diff --git a/test/memoize.js b/test/index.js similarity index 94% rename from test/memoize.js rename to test/index.js index d5c30cf..46263de 100644 --- a/test/memoize.js +++ b/test/index.js @@ -121,7 +121,7 @@ module.exports = function (t, a) { "Original arguments": function (a) { var fn, mfn, x = {}; fn = function (x, y) { x = y; return toArray(mfn.args); }; - mfn = t(fn); + mfn = t(fn, { resolvers: [] }); a.deep(mfn(23, 'raz', x), [23, 'raz', x]); }, @@ -320,7 +320,7 @@ module.exports = function (t, a) { "Async": { "Regular": { "Success": function (a, d) { - var mfn, fn, u = {}, i = 0; + var mfn, fn, u = {}, i = 0, invoked = 0; fn = function (x, y, cb) { nextTick(function () { ++i; @@ -332,45 +332,57 @@ module.exports = function (t, a) { mfn = t(fn, { async: true }); a(mfn(3, 7, function (err, res) { + ++invoked; a.deep([err, res], [null, 10], "Result #1"); }), u, "Initial"); a(mfn(3, 7, function (err, res) { + ++invoked; a.deep([err, res], [null, 10], "Result #2"); }), u, "Initial #2"); a(mfn(5, 8, function (err, res) { + ++invoked; a.deep([err, res], [null, 13], "Result B #1"); }), u, "Initial #2"); a(mfn(3, 7, function (err, res) { + ++invoked; a.deep([err, res], [null, 10], "Result #3"); }), u, "Initial #2"); a(mfn(5, 8, function (err, res) { + ++invoked; a.deep([err, res], [null, 13], "Result B #2"); }), u, "Initial #3"); nextTick(function () { - a(i, 2, "Called #2"); + a(i, 2, "Init Called"); + a(invoked, 5, "Cb Called"); a(mfn(3, 7, function (err, res) { + ++invoked; a.deep([err, res], [null, 10], "Again: Result"); }), u, "Again: Initial"); a(mfn(5, 8, function (err, res) { + ++invoked; a.deep([err, res], [null, 13], "Again B: Result"); }), u, "Again B: Initial"); nextTick(function () { - a(i, 2, "Again Called #2"); + a(i, 2, "Init Called #2"); + a(invoked, 7, "Cb Called #2"); mfn.clear(3, 7); a(mfn(3, 7, function (err, res) { + ++invoked; a.deep([err, res], [null, 10], "Again: Result"); }), u, "Again: Initial"); a(mfn(5, 8, function (err, res) { + ++invoked; a.deep([err, res], [null, 13], "Again B: Result"); }), u, "Again B: Initial"); nextTick(function () { - a(i, 3, "Call After clear"); + a(i, 3, "Init After clear"); + a(invoked, 9, "Cb After clear"); d(); }); }); @@ -870,35 +882,35 @@ module.exports = function (t, a) { a(mfn(5, 8), 13, "Result B #3"); a(i, 3, "Called B #3"); - a(mfn(77, 11), 88, "Result D #1"); // Clear 3, 7 + a(mfn(77, 11), 88, "Result D #1"); // Clear 12, 4 a(i, 4, "Called D #1"); a(mfn(5, 8), 13, "Result B #4"); a(i, 4, "Called B #4"); - a(mfn(12, 4), 16, "Result C #2"); - a(i, 4, "Called C #2"); + a(mfn(12, 4), 16, "Result C #2"); // Clear 3, 7 + a(i, 5, "Called C #2"); - a(mfn(3, 7), 10, "Result #5"); // Clear 5, 8 - a(i, 5, "Called #5"); - a(mfn(77, 11), 88, "Result D #2"); - a(i, 5, "Called D #2"); + a(mfn(3, 7), 10, "Result #5"); // Clear 77, 11 + a(i, 6, "Called #5"); + a(mfn(77, 11), 88, "Result D #2"); // Clear 5, 8 + a(i, 7, "Called D #2"); a(mfn(12, 4), 16, "Result C #3"); - a(i, 5, "Called C #3"); + a(i, 7, "Called C #3"); - a(mfn(5, 8), 13, "Result B #5"); // Clear 12, 4 - a(i, 6, "Called B #5"); + a(mfn(5, 8), 13, "Result B #5"); // Clear 3, 7 + a(i, 8, "Called B #5"); a(mfn(77, 11), 88, "Result D #3"); - a(i, 6, "Called D #3"); + a(i, 8, "Called D #3"); mfn.clear(77, 11); a(mfn(77, 11), 88, "Result D #4"); - a(i, 7, "Called D #4"); + a(i, 9, "Called D #4"); mfn.clearAll(); a(mfn(5, 8), 13, "Result B #6"); - a(i, 8, "Called B #6"); + a(i, 10, "Called B #6"); a(mfn(77, 11), 88, "Result D #5"); - a(i, 9, "Called D #5"); + a(i, 11, "Called D #5"); }, "Async": function (a, d) { var mfn, fn, u = {}, i = 0; @@ -954,49 +966,49 @@ module.exports = function (t, a) { a(mfn(12, 4, function (err, res) { a.deep([err, res], [null, 16], "Result C #2"); - a(i, 4, "Called C #2"); + a(i, 5, "Called C #2"); a(mfn(3, 7, function (err, res) { a.deep([err, res], [null, 10], "Result #5"); - a(i, 5, "Called #5"); + a(i, 6, "Called #5"); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #2"); - a(i, 5, "Called D #2"); + a(i, 7, "Called D #2"); a(mfn(12, 4, function (err, res) { a.deep([err, res], [null, 16], "Result C #3"); - a(i, 5, "Called C #3"); + a(i, 7, "Called C #3"); a(mfn(5, 8, function (err, res) { a.deep([err, res], [null, 13], "Result B #5"); - a(i, 6, "Called B #5"); + a(i, 8, "Called B #5"); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #3"); - a(i, 6, "Called D #3"); + a(i, 8, "Called D #3"); mfn.clear(77, 11); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #4"); - a(i, 7, "Called D #4"); + a(i, 9, "Called D #4"); mfn.clearAll(); a(mfn(5, 8, function (err, res) { a.deep([err, res], [null, 13], "Result B #6"); - a(i, 8, "Called B #6"); + a(i, 10, "Called B #6"); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #5"); - a(i, 9, "Called D #5"); + a(i, 11, "Called D #5"); d(); }), u, "Initial D #5"); @@ -1046,35 +1058,35 @@ module.exports = function (t, a) { a(mfn(5, 8), 13, "Result B #3"); a(i, 3, "Called B #3"); - a(mfn(77, 11), 88, "Result D #1"); + a(mfn(77, 11), 88, "Result D #1"); // Clear 12, 4 a(i, 4, "Called D #1"); a(mfn(5, 8), 13, "Result B #4"); a(i, 4, "Called B #4"); - a(mfn(12, 4), 16, "Result C #2"); - a(i, 4, "Called C #2"); + a(mfn(12, 4), 16, "Result C #2"); // Clear 3, 7 + a(i, 5, "Called C #2"); - a(mfn(3, 7), 10, "Result #5"); - a(i, 5, "Called #5"); - a(mfn(77, 11), 88, "Result D #2"); - a(i, 5, "Called D #2"); + a(mfn(3, 7), 10, "Result #5"); // Clear 77, 11 + a(i, 6, "Called #5"); + a(mfn(77, 11), 88, "Result D #2"); // Clear 5, 8 + a(i, 7, "Called D #2"); a(mfn(12, 4), 16, "Result C #3"); - a(i, 5, "Called C #3"); + a(i, 7, "Called C #3"); - a(mfn(5, 8), 13, "Result B #5"); - a(i, 6, "Called B #5"); + a(mfn(5, 8), 13, "Result B #5"); // Clear 3, 7 + a(i, 8, "Called B #5"); a(mfn(77, 11), 88, "Result D #3"); - a(i, 6, "Called D #3"); + a(i, 8, "Called D #3"); mfn.clear(77, 11); a(mfn(77, 11), 88, "Result D #4"); - a(i, 7, "Called D #4"); + a(i, 9, "Called D #4"); mfn.clearAll(); a(mfn(5, 8), 13, "Result B #6"); - a(i, 8, "Called B #6"); + a(i, 10, "Called B #6"); a(mfn(77, 11), 88, "Result D #5"); - a(i, 9, "Called D #5"); + a(i, 11, "Called D #5"); }, "Async": function (a, d) { var mfn, fn, u = {}, i = 0; @@ -1130,48 +1142,49 @@ module.exports = function (t, a) { a(mfn(12, 4, function (err, res) { a.deep([err, res], [null, 16], "Result C #2"); - a(i, 4, "Called C #2"); + a(i, 5, "Called C #2"); a(mfn(3, 7, function (err, res) { a.deep([err, res], [null, 10], "Result #5"); - a(i, 5, "Called #5"); + a(i, 6, "Called #5"); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #2"); - a(i, 5, "Called D #2"); + a(i, 7, "Called D #2"); a(mfn(12, 4, function (err, res) { a.deep([err, res], [null, 16], "Result C #3"); - a(i, 5, "Called C #3"); + a(i, 7, "Called C #3"); a(mfn(5, 8, function (err, res) { a.deep([err, res], [null, 13], "Result B #5"); - a(i, 6, "Called B #5"); + a(i, 8, "Called B #5"); + a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #3"); - a(i, 6, "Called D #3"); + a(i, 8, "Called D #3"); mfn.clear(77, 11); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #4"); - a(i, 7, "Called D #4"); + a(i, 9, "Called D #4"); mfn.clearAll(); a(mfn(5, 8, function (err, res) { a.deep([err, res], [null, 13], "Result B #6"); - a(i, 8, "Called B #6"); + a(i, 10, "Called B #6"); a(mfn(77, 11, function (err, res) { a.deep([err, res], [null, 88], "Result D #5"); - a(i, 9, "Called D #5"); + a(i, 11, "Called D #5"); d(); }), u, "Initial D #5"); diff --git a/test/primitive.js b/test/primitive.js new file mode 100644 index 0000000..949f485 --- /dev/null +++ b/test/primitive.js @@ -0,0 +1,41 @@ +'use strict'; + +module.exports = function (t, a) { + return { + "No args": function (a) { + var i = 0, fn = function () { ++i; return arguments[0]; }, mfn; + mfn = t(fn, { primitive: true }); + a(mfn('ble'), 'ble', "#1"); + a(mfn({}), 'ble', "#2"); + a(i, 1, "Called once"); + }, + "One arg": function (a) { + var i = 0, fn = function (x) { ++i; return x; }, mfn + , y = { toString: function () { return 'foo'; } }; + mfn = t(fn, { primitive: true }); + a(mfn(y), y, "#1"); + a(mfn('foo'), y, "#2"); + a(i, 1, "Called once"); + }, + "Many args": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , y = { toString: function () { return 'foo'; } }; + mfn = t(fn, { primitive: true }); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#1"); + a(mfn('foo', 'bar', 'zeta'), 'foobarzeta', "#2"); + a(i, 1, "Called once"); + }, + "Clear cache": function (a) { + var i = 0, fn = function (x, y, z) { ++i; return x + y + z; }, mfn + , y = { toString: function () { return 'foo'; } }; + mfn = t(fn, { primitive: true }); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#1"); + a(mfn('foo', 'bar', 'zeta'), 'foobarzeta', "#2"); + a(i, 1, "Called once"); + mfn.clear('foo', { toString: function () { return 'bar'; } }, + 'zeta'); + a(mfn(y, 'bar', 'zeta'), 'foobarzeta', "#3"); + a(i, 2, "Called twice"); + } + }; +}; diff --git a/test/regular.js b/test/regular.js new file mode 100644 index 0000000..b7674eb --- /dev/null +++ b/test/regular.js @@ -0,0 +1,175 @@ +'use strict'; + +var toArray = require('es5-ext/lib/Array/from'); + +module.exports = function (t, a) { + return { + "0": function () { + var i = 0, fn = function () { ++i; return 3; }; + + fn = t(fn); + a(fn(), 3, "First"); + a(fn(1), 3, "Second"); + a(fn(5), 3, "Third"); + a(i, 1, "Called once"); + }, + "1": function () { + var i = 0, fn = function (x) { ++i; return x; }; + + fn = t(fn); + return { + "No arg": function () { + i = 0; + a(fn(), undefined, "First"); + a(fn(), undefined, "Second"); + a(fn(), undefined, "Third"); + a(i, 1, "Called once"); + }, + "Arg": function () { + var x = {}; + i = 0; + a(fn(x, 8), x, "First"); + a(fn(x, 4), x, "Second"); + a(fn(x, 2), x, "Third"); + a(i, 1, "Called once"); + }, + "Other Arg": function () { + var x = {}; + i = 0; + a(fn(x, 2), x, "First"); + a(fn(x, 9), x, "Second"); + a(fn(x, 3), x, "Third"); + a(i, 1, "Called once"); + } + }; + }, + "3": function () { + var i = 0, fn = function (x, y, z) { ++i; return [x, y, z]; }, r; + + fn = t(fn); + return { + "No args": function () { + i = 0; + a.deep(r = fn(), [undefined, undefined, undefined], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(r = fn(x, 8), [x, 8, undefined], "First"); + a(fn(x, 8), r, "Second"); + a(fn(x, 8), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(r = fn(x, 5), [x, 5, undefined], "Second"); + a(fn(x, 5), r, "Third"); + a(i, 2, "Called once"); + } + }; + }, + "Full stuff": function () { + var x = {}; + i = 0; + a.deep(r = fn(x, 8, 23, 98), [x, 8, 23], "First"); + a(fn(x, 8, 23, 43), r, "Second"); + a(fn(x, 8, 23, 9), r, "Third"); + a(i, 1, "Called once"); + return { + "Other": function () { + a.deep(r = fn(x, 23, 8, 13), [x, 23, 8], "Second"); + a(fn(x, 23, 8, 22), r, "Third"); + a(i, 2, "Called once"); + } + }; + } + }; + }, + "Dynamic": function () { + var i = 0, fn = function () { ++i; return arguments; }, r; + + fn = t(fn, { length: false }); + return { + "No args": function () { + i = 0; + a.deep(toArray(r = fn()), [], "First"); + a(fn(), r, "Second"); + a(fn(), r, "Third"); + a(i, 1, "Called once"); + }, + "Some Args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(x, 8)), [x, 8], "First"); + a(fn(x, 8), r, "Second"); + a(fn(x, 8), r, "Third"); + a(i, 1, "Called once"); + }, + "Many args": function () { + var x = {}; + i = 0; + a.deep(toArray(r = fn(x, 8, 23, 98)), [x, 8, 23, 98], "First"); + a(fn(x, 8, 23, 98), r, "Second"); + a(fn(x, 8, 23, 98), r, "Third"); + a(i, 1, "Called once"); + } + }; + }, + "Clear Cache": { + "Specific": function () { + var i = 0, fn, mfn, x = {}; + + fn = function (a, b, c) { + if (c === 3) { + ++i; + } + return arguments; + }; + + mfn = t(fn); + mfn(1, x, 3); + mfn(1, x, 4); + mfn.clear(1, x, 4); + mfn(1, x, 3); + mfn(1, x, 3); + a(i, 1, "Pre clear"); + mfn.clear(1, x, 3); + mfn(1, x, 3); + a(i, 2, "After clear"); + + i = 0; + mfn = t(fn, { length: false }); + mfn(1, x, 3); + mfn(1, x, 3); + mfn(); + mfn(); + mfn.clear(); + mfn(1, x, 3); + a(i, 1, "Proper no arguments clear"); + }, + "All": function () { + var i = 0, fn, x = {}; + + fn = function () { + ++i; + return arguments; + }; + + fn = t(fn, { length: 3 }); + fn(1, x, 3); + fn(1, x, 4); + fn(1, x, 3); + fn(1, x, 4); + a(i, 2, "Pre clear"); + fn.clearAll(); + fn(1, x, 3); + fn(1, x, 4); + fn(1, x, 3); + fn(1, x, 4); + a(i, 4, "After clear"); + } + } + }; +};