diff --git a/lib/core.js b/lib/core.js index 05a4313..97842c3 100644 --- a/lib/core.js +++ b/lib/core.js @@ -2,6 +2,16 @@ var asap = require('asap') +// Use Symbol if it's available, but otherwise just +// generate a random string as this is probably going +// to be unique +var handleSymbol = typeof Symbol === 'function' ? + Symbol() : + ( + '_promise_internal_key_handle_' + + Math.random().toString(35).substr(2, 10) + '_' + ) + module.exports = Promise; function Promise(fn) { if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new') @@ -16,6 +26,7 @@ function Promise(fn) { handle(new Handler(onFulfilled, onRejected, resolve, reject)) }) } + this.then[handleSymbol] = handle; function handle(deferred) { if (state === null) { @@ -43,10 +54,32 @@ function Promise(fn) { function resolve(newValue) { try { //Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.') + if (newValue instanceof Promise && newValue.handle && typeof newValue.handle === 'function') { + // to prevent a memory leak, we adopt the value of the other promise + // allowing this promise to be garbage collected as soon as nobody + // has a reference to it + self.handle = newValue.handle; + self.then = newValue.then; + deferreds.forEach(function (deferred) { + self.handle(deferred); + }); + return + } if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { var then = newValue.then if (typeof then === 'function') { - doResolve(then.bind(newValue), resolve, reject) + if (typeof then[handleSymbol] === 'function') { + // to prevent a memory leak, we adopt the value of the other promise + // allowing this promise to be garbage collected as soon as nobody + // has a reference to it + handle = (self[handleSymbol] = then[handleSymbol]); + self.then = then; + deferreds.forEach(function (deferred) { + handle(deferred); + }); + } else { + doResolve(then.bind(newValue), resolve, reject) + } return } } diff --git a/package.json b/package.json index 996a9f9..5d28ad3 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,9 @@ "description": "Bare bones Promises/A+ implementation", "main": "index.js", "scripts": { - "test": "mocha --timeout 200 --slow 99999 && npm run test-memory-leak", - "test-resolve": "mocha test/resolver-tests.js -R spec --timeout 200 --slow 999999", - "test-extensions": "mocha test/extensions-tests.js -R spec --timeout 200 --slow 999999", + "test": "mocha --timeout 200 --slow 99999 -R dot && npm run test-memory-leak", + "test-resolve": "mocha test/resolver-tests.js --timeout 200 --slow 999999", + "test-extensions": "mocha test/extensions-tests.js --timeout 200 --slow 999999", "test-memory-leak": "node --expose-gc test/memory-leak.js" }, "repository": { diff --git a/test/memory-leak.js b/test/memory-leak.js index 80daabc..1b87abf 100644 --- a/test/memory-leak.js +++ b/test/memory-leak.js @@ -23,7 +23,9 @@ function next() { if (typeof global.gc === 'function') { global.gc() sampleB = process.memoryUsage() + console.log('Memory usage at start:'); console.dir(sampleA) + console.log('Memory usage at end:'); console.dir(sampleB) assert(sampleA.rss * 1.2 > sampleB.rss, 'RSS should not grow by more than 20%') assert(sampleA.heapTotal * 1.2 > sampleB.heapTotal, 'heapTotal should not grow by more than 20%')