From 9cf8df8c22b531905ec2fd2ac191cd93979adbbd Mon Sep 17 00:00:00 2001 From: Kirill Efimov Date: Fri, 22 Jan 2016 20:32:10 +0300 Subject: [PATCH] Added promise support --- .lint | 4 +- ext/dispose.js | 2 +- ext/promise.js | 50 +++++++ index.js | 1 + lib/configure-map.js | 6 +- package.json | 1 + test/ext/max-age.js | 57 +++++++- test/ext/promise.js | 302 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 417 insertions(+), 6 deletions(-) create mode 100644 ext/promise.js create mode 100644 test/ext/promise.js diff --git a/.lint b/.lint index 1ddad3c..a51f462 100644 --- a/.lint +++ b/.lint @@ -24,7 +24,9 @@ predef+ setTimeout, clearTimeout ./test/index.js ./test/ext/max-age.js -predef+ setTimeout +predef+ setTimeout, Promise, global +./test/ext/promise.js +predef+ Promise, setTimeout, global ./benchmark sub diff --git a/ext/dispose.js b/ext/dispose.js index 4002b7f..c2aeda8 100644 --- a/ext/dispose.js +++ b/ext/dispose.js @@ -11,7 +11,7 @@ var callable = require('es5-ext/object/valid-callable') extensions.dispose = function (dispose, conf, options) { var del; callable(dispose); - if (options.async && extensions.async) { + if ((options.async && extensions.async) || (options.promise && extensions.promise)) { conf.on('deleteasync', del = function (id, result) { apply.call(dispose, null, slice.call(result.args, 1)); }); diff --git a/ext/promise.js b/ext/promise.js new file mode 100644 index 0000000..6f58e92 --- /dev/null +++ b/ext/promise.js @@ -0,0 +1,50 @@ +// Support for functions returning promise + +'use strict'; + +var nextTick = require('next-tick') + + , create = Object.create; + +require('../lib/registered-extensions').promise = function (tbi, conf) { + var cache = create(null); + + // After not from cache call + conf.on('set', function (id, __, promise) { + promise.then(function () { + // nextTick avoids error interception + nextTick(function () { + cache[id] = promise; + conf.emit('setasync', id, 1); + }); + }, function () { + nextTick(function () { + conf.delete(id); + }); + }); + }); + + // From cache (sync) + conf.on('get', function (id, args, context) { + conf.emit('getasync', id, args, context); + }); + + // On delete + conf.on('delete', function (id) { + var result; + // If false, we don't have value yet, so we assume that intention is not + // to memoize this call. After value is obtained we don't cache it but + // gracefully pass to callback + if (!cache[id]) return; + result = cache[id]; + delete cache[id]; + conf.emit('deleteasync', id, result); + }); + + // On clear + conf.on('clear', function () { + var oldCache = cache; + cache = create(null); + conf.emit('clearasync', oldCache); + }); +}; diff --git a/index.js b/index.js index 660d51e..fac3798 100644 --- a/index.js +++ b/index.js @@ -26,6 +26,7 @@ module.exports = function (fn/*, options*/) { // Assure extensions if (options.async) require('./ext/async'); + if (options.promise) require('./ext/promise'); if (options.dispose) require('./ext/dispose'); if (options.maxAge) require('./ext/max-age'); if (options.max) require('./ext/max'); diff --git a/lib/configure-map.js b/lib/configure-map.js index 522910a..edcd61e 100644 --- a/lib/configure-map.js +++ b/lib/configure-map.js @@ -49,7 +49,7 @@ module.exports = function (original, length, options) { throw customError("Circular invocation", 'CIRCULAR_INVOCATION'); } cache[id] = result; - if (setListeners) conf.emit('set', id); + if (setListeners) conf.emit('set', id, null, result); return result; }, memLength); } else if (length === 0) { @@ -65,7 +65,7 @@ module.exports = function (original, length, options) { throw customError("Circular invocation", 'CIRCULAR_INVOCATION'); } cache.data = result; - if (setListeners) conf.emit('set', 'data'); + if (setListeners) conf.emit('set', 'data', null, result); return result; }; } else { @@ -83,7 +83,7 @@ module.exports = function (original, length, options) { throw customError("Circular invocation", 'CIRCULAR_INVOCATION'); } cache[id] = result; - if (setListeners) conf.emit('set', id); + if (setListeners) conf.emit('set', id, null, result); return result; }; } diff --git a/package.json b/package.json index 4980891..949a544 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "timers-ext": "0.1" }, "devDependencies": { + "plain-promise": "^0.1.1", "tad": "~0.2.3", "xlint": "~0.2.2", "xlint-jslint-medikoo": "~0.1.4" diff --git a/test/ext/max-age.js b/test/ext/max-age.js index 5948d69..e2eb205 100644 --- a/test/ext/max-age.js +++ b/test/ext/max-age.js @@ -1,7 +1,8 @@ 'use strict'; var memoize = require('../..') - , nextTick = require('next-tick'); + , nextTick = require('next-tick') + , Promise = global.Promise || require('plain-promise'); require('../../ext/async'); @@ -52,6 +53,60 @@ module.exports = function () { }, 90); }, 20); }, + Promise: function (a, d) { + var mfn, fn, i = 0; + fn = function (x, y) { + return new Promise(function (res) { + ++i; + res(x + y); + }); + }; + + mfn = memoize(fn, { promise: true, maxAge: 100 }); + + mfn(3, 7).then(function (res) { + a(res, 10, "Result #1"); + }); + mfn(5, 8).then(function (res) { + a(res, 13, "Result #2"); + }); + mfn(3, 7).then(function (res) { + a(res, 10, "Result #3"); + }); + mfn(3, 7).then(function (res) { + a(res, 10, "Result #4"); + }); + mfn(5, 8).then(function (res) { + a(res, 13, "Result #5"); + }); + + setTimeout(function () { + a(i, 2, "Called #2"); + + mfn(3, 7).then(function (res) { + a(res, 10, "Again: Result #1"); + }); + mfn(5, 8).then(function (res) { + a(res, 13, "Again: Result #2"); + }); + + setTimeout(function () { + a(i, 2, "Again Called #2"); + + mfn(3, 7).then(function (res) { + a(res, 10, "Again: Result #1"); + }); + mfn(5, 8).then(function (res) { + a(res, 13, "Again: Result #2"); + }); + + nextTick(function () { + a(i, 4, "Call After clear"); + d(); + }); + }, 100); + }, 20); + }, Async: function (a, d) { var mfn, fn, u = {}, i = 0; fn = function (x, y, cb) { diff --git a/test/ext/promise.js b/test/ext/promise.js new file mode 100644 index 0000000..f458e71 --- /dev/null +++ b/test/ext/promise.js @@ -0,0 +1,302 @@ +'use strict'; + +var memoize = require('../..') + , Promise = global.Promise || require('plain-promise') + , nextTick = require('next-tick'); + +module.exports = function () { + return { + Regular: { + Success: function (a, d) { + var mfn, fn, i = 0, invoked = 0; + fn = function (x, y) { + return new Promise(function (res) { + ++i; + res(x + y); + }); + }; + + mfn = memoize(fn, { promise: true }); + + mfn(3, 7).then(function (res) { + ++invoked; + a(res, 10, "Result #1"); + }, a.never); + + mfn(3, 7).then(function (res) { + ++invoked; + a(res, 10, "Result #2"); + }, a.never); + + mfn(5, 8).then(function (res) { + ++invoked; + a(res, 13, "Result B #1"); + }, a.never); + + mfn(3, 7).then(function (res) { + ++invoked; + a(res, 10, "Result #3"); + }, a.never); + + mfn(5, 8).then(function (res) { + ++invoked; + a(res, 13, "Result B #2"); + }, a.never); + + setTimeout(function () { + a(i, 2, "Init Called"); + a(invoked, 5, "Cb Called"); + + mfn(3, 7).then(function (res) { + ++invoked; + a(res, 10, "Again: Result"); + }, a.never); + + mfn(5, 8).then(function (res) { + ++invoked; + a(res, 13, "Again B: Result"); + }, a.never); + + setTimeout(function () { + a(i, 2, "Init Called #2"); + a(invoked, 7, "Cb Called #2"); + + mfn.delete(3, 7); + + mfn(3, 7).then(function (res) { + ++invoked; + a(res, 10, "Again: Result"); + }, a.never); + + mfn(5, 8).then(function (res) { + ++invoked; + a(res, 13, "Again B: Result"); + }, a.never); + + setTimeout(function () { + a(i, 3, "Init After delete"); + a(invoked, 9, "Cb After delete"); + d(); + }, 10); + }, 10); + }, 10); + }, + Error: function (a, d) { + var mfn, fn, i = 0, e = new Error("Test"); + fn = function (x, y) { + return new Promise(function (res, rej) { + ++i; + rej(e); + }); + }; + + mfn = memoize(fn, { promise: true, dispose: a.never }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #1"); + }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #2"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Result B #1"); + }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #3"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Result B #2"); + }); + + setTimeout(function () { + a(i, 2, 'Called #2'); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Again: Result"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Again B: Result"); + }); + + setTimeout(function (err) { + a(i, 4, "Again Called #2"); + d(); + }, 10); + }, 10); + } + }, + Primitive: { + Success: function (a, d) { + var mfn, fn, i = 0; + fn = function (x, y) { + return new Promise(function (res) { + ++i; + res(x + y); + }); + }; + + mfn = memoize(fn, { promise: true, primitive: true }); + + mfn(3, 7).then(function (res) { + a(res, 10, "Result #1"); + }, a.never); + + mfn(3, 7).then(function (res) { + a(res, 10, "Result #2"); + }, a.never); + + mfn(5, 8).then(function (res) { + a(res, 13, "Result B #1"); + }, a.never); + + mfn(3, 7).then(function (res) { + a(res, 10, "Result #3"); + }, a.never); + + mfn(5, 8).then(function (res) { + a(res, 13, "Result B #2"); + }, a.never); + + setTimeout(function () { + a(i, 2, "Called #2"); + + mfn(3, 7).then(function (res) { + a(res, 10, "Again: Result"); + }, a.never); + + mfn(5, 8).then(function (res) { + a(res, 13, "Again B: Result"); + }, a.never); + + setTimeout(function () { + a(i, 2, "Again Called #2"); + + mfn.delete(3, 7); + + mfn(3, 7).then(function (res) { + a(res, 10, "Again: Result"); + }, a.never); + + mfn(5, 8).then(function (res) { + a(res, 13, "Again B: Result"); + }, a.never); + + setTimeout(function () { + a(i, 3, "Call After delete"); + d(); + }, 10); + }, 10); + }, 10); + }, + Error: function (a, d) { + var mfn, fn, i = 0, e = new Error("Test"); + fn = function (x, y) { + return new Promise(function (res, rej) { + ++i; + rej(e); + }); + }; + + mfn = memoize(fn, { promise: true, primitive: true }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #1"); + }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #2"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Result B #1"); + }); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Result #3"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Result B #2"); + }); + + setTimeout(function () { + a(i, 2, 'Called #2'); + + mfn(3, 7).then(a.never, function (err) { + a(err, e, "Again: Result"); + }); + + mfn(5, 8).then(a.never, function (err) { + a(err, e, "Again B: Result"); + }); + + setTimeout(function (err) { + a(i, 4, "Again Called #2"); + d(); + }, 10); + }, 10); + }, + "Primitive null arg case": function (a, d) { + var mfn, x = {}; + mfn = memoize(function f(id) { + return new Promise(function (res) { res(x); }); + }, { + promise: true, + primitive: true + }); + + mfn(null).then(function (res) { + a.deep(res, x, "Args"); + d(); + }, a.never); + } + }, + "Sync Clear": function (a, d) { + var mfn, fn; + fn = function (x) { + return new Promise(function (res) { + nextTick(function () { + res(x); + }); + }); + }; + + mfn = memoize(fn, { promise: true }); + + mfn(1).then(function (res) { + a(res, 1, "First"); + }, a.never); + + mfn(2).then(function (res) { + a(res, 2, "Second"); + d(); + }, a.never); + }, + "Sync Clear: Primitive": function (a, d) { + var mfn, fn; + fn = function (x) { + return new Promise(function (res) { + nextTick(function () { + res(x); + }); + }); + }; + + mfn = memoize(fn, { promise: true, primitive: true }); + + mfn(1).then(function (res) { + a(res, 1, "First"); + }, a.never); + + mfn(2).then(function (res) { + a(res, 2, "Second"); + d(); + }, a.never); + } + }; +};