From 932abc1076a1e2dfff07dc53ca172aa2167a8d1f Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 20 Jun 2016 02:08:19 +0200 Subject: [PATCH 01/13] make npm scripts work under Windows * fix quotes of argument to uglify * not sure whether specifying the path node_modules/mocha/bin/ is an antipattern (some comments by npm authors claim so) but istanbul does not seem to take the PATH into account --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9b83e00..0a7d9e3 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,12 @@ "compile": "npm run compile-src", "compile-src": "mkdirp build/src && buble -m -i src -o build/src --no modules", "build-dist": "npm run compile && mkdirp dist && rollup -c", - "build": "npm run build-dist && uglifyjs -c 'warnings=false' -m -o dist/creed.min.js -- dist/creed.js", + "build": "npm run build-dist && uglifyjs -c \"warnings=false\" -m -o dist/creed.min.js -- dist/creed.js", "preversion": "npm run build", "check-coverage": "istanbul check-coverage --statements 100 --branches 100 --lines 100 --functions 100 coverage/coverage*.json", "lint": "jsinspect src && eslint src", "pretest": "npm run lint", - "test": "istanbul cover _mocha", + "test": "istanbul cover node_modules/mocha/bin/_mocha", "posttest": "npm run test-aplus", "test-aplus": "promises-aplus-tests test/aplus.js --reporter dot" }, From 3c3ead72448ab2a2e8a0ba8fef37164e820ac885 Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 20 Jun 2016 02:15:59 +0200 Subject: [PATCH 02/13] generalise actions --- src/Action.js | 28 ++++++++++++++++++++++++++++ src/chain.js | 26 +++++++++----------------- src/coroutine.js | 24 ++++++++++-------------- src/delay.js | 10 ++++------ src/inspect.js | 12 +++++------- src/iterable.js | 23 ++++++++++++++--------- src/map.js | 17 +++++++---------- src/then.js | 34 ++++++++++++++++------------------ src/timeout.js | 8 ++++---- 9 files changed, 97 insertions(+), 85 deletions(-) create mode 100644 src/Action.js diff --git a/src/Action.js b/src/Action.js new file mode 100644 index 0000000..72a3f11 --- /dev/null +++ b/src/Action.js @@ -0,0 +1,28 @@ +export default class Action { + constructor (promise) { + this.promise = promise + } + + // default onFulfilled action + /* istanbul ignore next */ + fulfilled (p) { + this.promise._become(p) + } + + // default onRejected action + rejected (p) { + this.promise._become(p) + return false + } + + tryCall (f, x) { + let result + try { + result = f(x) + } catch (e) { + this.promise._reject(e) + return + } // else + this.handle(result) + } +} diff --git a/src/chain.js b/src/chain.js index 9f48b5a..ae46bcf 100644 --- a/src/chain.js +++ b/src/chain.js @@ -1,3 +1,4 @@ +import Action from './Action' import maybeThenable from './maybeThenable' export default function (f, p, promise) { @@ -5,30 +6,21 @@ export default function (f, p, promise) { return promise } -class Chain { +class Chain extends Action { constructor (f, promise) { + super(promise) this.f = f - this.promise = promise } fulfilled (p) { - try { - runChain(this.f, p.value, this.promise) - } catch (e) { - this.promise._reject(e) - } + this.tryCall(this.f, p.value) } - rejected (p) { - this.promise._become(p) - } -} + handle (y) { + if (!(maybeThenable(y) && typeof y.then === 'function')) { + this.promise._reject(new TypeError('f must return a promise')) + } -function runChain (f, x, p) { - const y = f(x) - if (!(maybeThenable(y) && typeof y.then === 'function')) { - throw new TypeError('f must return a promise') + this.promise._resolve(y) } - - p._resolve(y) } diff --git a/src/coroutine.js b/src/coroutine.js index cee3dc4..098e262 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -1,25 +1,21 @@ +import Action from './Action' + export default function (resolve, iterator, promise) { new Coroutine(resolve, iterator, promise).run() + // taskQueue.add(new Coroutine(resolve, iterator, promise)) return promise } -class Coroutine { +class Coroutine extends Action { constructor (resolve, iterator, promise) { + super(promise) this.resolve = resolve - this.iterator = iterator - this.promise = promise + this.next = iterator.next.bind(iterator) + this.throw = iterator.throw.bind(iterator) } run () { - this.step(this.iterator.next, void 0) - } - - step (continuation, x) { - try { - this.handle(continuation.call(this.iterator, x)) - } catch (e) { - this.promise._reject(e) - } + this.tryCall(this.next, void 0) } handle (result) { @@ -31,11 +27,11 @@ class Coroutine { } fulfilled (ref) { - this.step(this.iterator.next, ref.value) + this.tryCall(this.next, ref.value) } rejected (ref) { - this.step(this.iterator.throw, ref.value) + this.tryCall(this.throw, ref.value) return true } } diff --git a/src/delay.js b/src/delay.js index fdb5827..cef6765 100644 --- a/src/delay.js +++ b/src/delay.js @@ -1,22 +1,20 @@ +import Action from './Action' + export default function (ms, p, promise) { p._runAction(new Delay(ms, promise)) return promise } -class Delay { +class Delay extends Action { constructor (time, promise) { + super(promise) this.time = time - this.promise = promise } fulfilled (p) { /*global setTimeout*/ setTimeout(become, this.time, p, this.promise) } - - rejected (p) { - this.promise._become(p) - } } function become (p, promise) { diff --git a/src/inspect.js b/src/inspect.js index 951de89..bfe767d 100644 --- a/src/inspect.js +++ b/src/inspect.js @@ -1,4 +1,5 @@ import { PENDING, FULFILLED, REJECTED, SETTLED, NEVER, HANDLED } from './state' +import Action from './Action' export function isPending (p) { return (p.state() & PENDING) > 0 @@ -47,11 +48,8 @@ export function silenceError (p) { p._runAction(silencer) } -const silencer = { - fulfilled () {}, - rejected: setHandled -} - -function setHandled (rejected) { - rejected._state |= HANDLED +const silencer = new Action(null) +silencer.fulfilled = function fulfilled (p) { } +silencer.rejected = function setHandled (p) { + p._state |= HANDLED } diff --git a/src/iterable.js b/src/iterable.js index d3eeec2..a9d3c8b 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -1,4 +1,5 @@ import { isFulfilled, isRejected, silenceError } from './inspect' +import Action from './Action' import maybeThenable from './maybeThenable' export function resultsArray (iterable) { @@ -58,18 +59,22 @@ function handleItem (resolve, handler, x, i, promise) { } else if (isRejected(p)) { handler.rejectAt(p, i, promise) } else { - settleAt(p, handler, i, promise) + p._runAction(new Indexed(handler, i, promise)) } } -function settleAt (p, handler, i, promise) { - p._runAction({handler, i, promise, fulfilled, rejected}) -} +class Indexed extends Action { + constructor (handler, i, promise) { + super(promise) + this.i = i + this.handler = handler + } -function fulfilled (p) { - this.handler.fulfillAt(p, this.i, this.promise) -} + fulfilled (p) { + this.handler.fulfillAt(p, this.i, this.promise) + } -function rejected (p) { - return this.handler.rejectAt(p, this.i, this.promise) + rejected (p) { + return this.handler.rejectAt(p, this.i, this.promise) + } } diff --git a/src/map.js b/src/map.js index e0469b5..8ec6f6b 100644 --- a/src/map.js +++ b/src/map.js @@ -1,24 +1,21 @@ +import Action from './Action' + export default function (f, p, promise) { p._when(new Map(f, promise)) return promise } -class Map { +class Map extends Action { constructor (f, promise) { + super(promise) this.f = f - this.promise = promise } fulfilled (p) { - try { - const f = this.f - this.promise._fulfill(f(p.value)) - } catch (e) { - this.promise._reject(e) - } + this.tryCall(this.f, p.value) } - rejected (p) { - this.promise._become(p) + handle (result) { + this.promise._fulfill(result) } } diff --git a/src/then.js b/src/then.js index 435fd54..deaff82 100644 --- a/src/then.js +++ b/src/then.js @@ -1,38 +1,36 @@ +import Action from './Action' + export default function then (f, r, p, promise) { p._when(new Then(f, r, promise)) return promise } -class Then { +class Then extends Action { constructor (f, r, promise) { + super(promise) this.f = f this.r = r - this.promise = promise } fulfilled (p) { - runThen(this.f, p, this.promise) + this.runThen(this.f, p) } rejected (p) { - return runThen(this.r, p, this.promise) + return this.runThen(this.r, p) } -} -function runThen (f, p, promise) { - if (typeof f !== 'function') { - promise._become(p) - return false + runThen (f, p) { + if (typeof f !== 'function') { + this.promise._become(p) + return false + } else { + this.tryCall(f, p.value) + return true + } } - tryMapNext(f, p.value, promise) - return true -} - -function tryMapNext (f, x, promise) { - try { - promise._resolve(f(x)) - } catch (e) { - promise._reject(e) + handle (result) { + this.promise._resolve(result) } } diff --git a/src/timeout.js b/src/timeout.js index adce7de..75b8e66 100644 --- a/src/timeout.js +++ b/src/timeout.js @@ -1,3 +1,4 @@ +import Action from './Action' import TimeoutError from './TimeoutError' export default function (ms, p, promise) { @@ -6,10 +7,10 @@ export default function (ms, p, promise) { return promise } -class Timeout { +class Timeout extends Action { constructor (timer, promise) { + super(promise) this.timer = timer - this.promise = promise } fulfilled (p) { @@ -19,8 +20,7 @@ class Timeout { rejected (p) { clearTimeout(this.timer) - this.promise._become(p) - return false + return super.rejected(p) } } From 508bbad9966d285f1f9e5363463370feca2bd367 Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 11 Jul 2016 20:34:21 +0200 Subject: [PATCH 03/13] fixes and additions to test suite --- .../promises-creed-algebraic.js | 2 +- test/Promise-test.js | 66 ++- test/concat-test.js | 14 +- test/fulfill-test.js | 2 +- test/future-test.js | 387 +++++++++++++++--- test/lib/test-util.js | 3 +- test/of-test.js | 16 +- test/reject-test.js | 8 +- test/then-test.js | 2 +- 9 files changed, 401 insertions(+), 99 deletions(-) diff --git a/perf/doxbee-sequential/promises-creed-algebraic.js b/perf/doxbee-sequential/promises-creed-algebraic.js index 9ab6b5d..94c664d 100644 --- a/perf/doxbee-sequential/promises-creed-algebraic.js +++ b/perf/doxbee-sequential/promises-creed-algebraic.js @@ -52,7 +52,7 @@ module.exports = function upload(stream, idOrPath, tag, done) { }).chain(function() { return File.whereUpdate({id: fileId}, {version: version.id}) .execWithin(tx); - }).map(function() { + }).then(function() { tx.commit(); return done(); }, function(err) { diff --git a/test/Promise-test.js b/test/Promise-test.js index 6b4dc5e..e9b288e 100644 --- a/test/Promise-test.js +++ b/test/Promise-test.js @@ -14,32 +14,58 @@ describe('Promise', () => { }) it('should reject if resolver throws synchronously', () => { - let expected = new Error() + const expected = new Error() return new Promise(() => { throw expected }) .then(assert.ifError, x => assert.strictEqual(expected, x)) }) - it('should fulfill with value', () => { - let expected = {} - return new Promise(resolve => resolve(expected)) - .then(x => assert.strictEqual(expected, x)) - }) + describe('resolvers', () => { + it('should fulfill with value', () => { + const expected = {} + return new Promise(resolve => resolve(expected)) + .then(x => assert.strictEqual(expected, x)) + }) - it('should resolve to fulfilled promise', () => { - let expected = {} - return new Promise(resolve => resolve(fulfill(expected))) - .then(x => assert.strictEqual(expected, x)) - }) + it('should resolve to fulfilled promise', () => { + const expected = {} + return new Promise(resolve => resolve(fulfill(expected))) + .then(x => assert.strictEqual(expected, x)) + }) - it('should resolve to rejected promise', () => { - let expected = {} - return new Promise(resolve => resolve(reject(expected))) - .then(assert.ifError, x => assert.strictEqual(expected, x)) - }) + it('should resolve to rejected promise', () => { + const expected = new Error() + return new Promise(resolve => resolve(reject(expected))) + .then(assert.ifError, x => assert.strictEqual(expected, x)) + }) - it('should reject with value', () => { - let expected = {} - return new Promise((resolve, reject) => reject(expected)) - .then(assert.ifError, x => assert.strictEqual(expected, x)) + it('should reject with value', () => { + const expected = new Error() + return new Promise((resolve, reject) => reject(expected)) + .then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + + it('should asynchronously fulfill with value', () => { + const expected = {} + return new Promise(resolve => setTimeout(resolve, 1, expected)) + .then(x => assert.strictEqual(expected, x)) + }) + + it('should asynchronously resolve to fulfilled promise', () => { + const expected = {} + return new Promise(resolve => setTimeout(resolve, 1, fulfill(expected))) + .then(x => assert.strictEqual(expected, x)) + }) + + it('should asynchronously resolve to rejected promise', () => { + const expected = new Error() + return new Promise(resolve => setTimeout(resolve, 1, reject(expected))) + .then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + + it('should asynchronously reject with value', () => { + const expected = new Error() + return new Promise((resolve, reject) => setTimeout(reject, 1, reject(expected))) + .then(assert.ifError, x => assert.strictEqual(expected, x)) + }) }) }) diff --git a/test/concat-test.js b/test/concat-test.js index afa61dd..dc22460 100644 --- a/test/concat-test.js +++ b/test/concat-test.js @@ -22,26 +22,32 @@ describe('concat', function () { assert.strictEqual(p2, p1.concat(p2)) }) - it('should return earlier future', () => { + it('should behave like earlier future', () => { + const expected = {} + const p = delay(1, expected).concat(delay(10)) + return assertSame(p, fulfill(expected)) + }) + + it('should behave like other earlier future', () => { const expected = {} const p = delay(10).concat(delay(1, expected)) return assertSame(p, fulfill(expected)) }) - it('should behave like fulfilled', () => { + it('should return other with fulfilled', () => { const expected = {} const p = fulfill(expected) return assert.strictEqual(delay(10).concat(p), p) }) - it('should behave like rejected', () => { + it('should return other with rejected', () => { const expected = {} const p = reject(expected) silenceError(p) return assert.strictEqual(delay(10).concat(p), p) }) - it('should behave like never', () => { + it('should be identity with never', () => { const p2 = never() const p1 = delay(10) return assert.strictEqual(p1.concat(p2), p1) diff --git a/test/fulfill-test.js b/test/fulfill-test.js index ad5d11d..c6342a2 100644 --- a/test/fulfill-test.js +++ b/test/fulfill-test.js @@ -30,7 +30,7 @@ describe('fulfill', () => { assert.strictEqual(p, p.catch(assert.ifError)) }) - it('then should be identity when typeof f !== function', () => { + it('then should be identity without f callback', () => { const p = fulfill(true) assert.strictEqual(p, p.then()) }) diff --git a/test/future-test.js b/test/future-test.js index 7691849..55a5470 100644 --- a/test/future-test.js +++ b/test/future-test.js @@ -1,11 +1,14 @@ import { describe, it } from 'mocha' -import { future, reject, fulfill, never, Future } from '../src/Promise' +import { future, reject, fulfill, isSettled, isPending, never } from '../src/main' +import { Future } from '../src/Promise' import { silenceError } from '../src/inspect' import { assertSame } from './lib/test-util' import assert from 'assert' +const silenced = p => (silenceError(p), p) const f = x => x + 1 const fp = x => fulfill(x + 1) +const rp = x => silenced(reject(x)) describe('future', () => { it('should return { resolve, promise }', () => { @@ -50,77 +53,117 @@ describe('future', () => { describe('state', () => { it('should have fulfilled state', () => { const { resolve, promise } = future() - const p = fulfill(1) resolve(p) - assert.equal(p.state(), promise.state()) + assert.strictEqual(p.state(), promise.state()) }) it('should have rejected state', () => { const { resolve, promise } = future() - - const p = reject(1) - silenceError(p) + const p = silenced(reject(1)) resolve(p) - assert.equal(p.state(), promise.state()) + assert.strictEqual(p.state(), promise.state()) }) it('should have never state', () => { const { resolve, promise } = future() - const p = never() resolve(p) - assert.equal(p.state(), promise.state()) + assert.strictEqual(p.state(), promise.state()) }) }) describe('inspect', () => { it('should have fulfilled state', () => { const { resolve, promise } = future() - const p = fulfill(1) resolve(p) - assert.equal(p.inspect(), promise.inspect()) + assert.strictEqual(p.inspect(), promise.inspect()) }) it('should have rejected state', () => { const { resolve, promise } = future() - - const p = reject(1) - silenceError(p) + const p = silenced(reject(1)) resolve(p) - assert.equal(p.inspect(), promise.inspect()) + assert.strictEqual(p.inspect(), promise.inspect()) }) it('should have never state', () => { const { resolve, promise } = future() + const p = never() + resolve(p) + assert.strictEqual(p.inspect(), promise.inspect()) + }) + }) + + describe('then', () => { + it('should behave like mapped for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + resolve(p) + return assertSame(p.map(f), promise.then(f)) + }) + + it('should behave like chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + resolve(p) + return assertSame(p.chain(fp), promise.then(fp)) + }) + + it('should behave like rejection chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + resolve(p) + return assertSame(p.chain(rp), promise.then(rp)) + }) + + it('should be identity for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(1)) + resolve(p) + assert.strictEqual(p, promise.then(f)) + }) + it('should be identity for never', () => { + const { resolve, promise } = future() const p = never() resolve(p) - assert.equal(p.inspect(), promise.inspect()) + assert.strictEqual(p, promise.then(f)) }) }) describe('catch', () => { - it('should behave like fulfilled', () => { + it('should be identity for fulfill', () => { const { resolve, promise } = future() - const p = fulfill(1) resolve(p) assert.strictEqual(p, promise.catch(f)) }) - it('should have rejected state', () => { + it('should behave like mapped for reject', () => { const { resolve, promise } = future() - const p = reject(1) resolve(p) return assertSame(p.catch(f), promise.catch(f)) }) - it('should have never state', () => { + it('should behave like chained for reject', () => { const { resolve, promise } = future() + const p = reject(1) + resolve(p) + return assertSame(p.catch(fp), promise.catch(fp)) + }) + it('should behave like rejection chained for reject', () => { + const { resolve, promise } = future() + const p = reject(1) + resolve(p) + return assertSame(p.catch(rp), promise.catch(rp)) + }) + + it('should be identity for never', () => { + const { resolve, promise } = future() const p = never() resolve(p) assert.strictEqual(p, promise.catch(f)) @@ -128,26 +171,22 @@ describe('future', () => { }) describe('map', () => { - it('should behave like fulfilled', () => { + it('should behave like mapped for fulfill', () => { const { resolve, promise } = future() - const p = fulfill(1) resolve(p) return assertSame(p.map(f), promise.map(f)) }) - it('should have rejected state', () => { + it('should be identity for reject', () => { const { resolve, promise } = future() - - const p = reject(1) - silenceError(p) + const p = silenced(reject(1)) resolve(p) assert.strictEqual(p, promise.map(f)) }) - it('should have never state', () => { + it('should be identity for never', () => { const { resolve, promise } = future() - const p = never() resolve(p) assert.strictEqual(p, promise.map(f)) @@ -155,26 +194,29 @@ describe('future', () => { }) describe('chain', () => { - it('should behave like fulfilled', () => { + it('should behave like chained for fulfill', () => { const { resolve, promise } = future() - const p = fulfill(1) resolve(p) return assertSame(p.chain(fp), promise.chain(fp)) }) - it('should have rejected state', () => { + it('should behave like rejection chained for fulfill', () => { const { resolve, promise } = future() + const p = fulfill(1) + resolve(p) + return assertSame(p.chain(rp), promise.chain(rp)) + }) - const p = reject(1) - silenceError(p) + it('should be identity for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(1)) resolve(p) assert.strictEqual(p, promise.chain(fp)) }) - it('should have never state', () => { + it('should be identity for never', () => { const { resolve, promise } = future() - const p = never() resolve(p) assert.strictEqual(p, promise.chain(fp)) @@ -182,27 +224,23 @@ describe('future', () => { }) describe('ap', () => { - it('should behave like fulfilled', () => { + it('should behave like apply for fulfill', () => { const { resolve, promise } = future() - const p = fulfill(f) const q = fulfill(1) resolve(p) return assertSame(p.ap(q), promise.ap(q)) }) - it('should behave like rejected', () => { + it('should be identity for reject', () => { const { resolve, promise } = future() - - const p = reject(f) - silenceError(p) + const p = silenced(reject(f)) resolve(p) assert.strictEqual(p, promise.ap(fulfill(1))) }) - it('should behave like never', () => { + it('should be identity for never', () => { const { resolve, promise } = future() - const p = never() resolve(p) return assert.strictEqual(p, promise.ap(fulfill(1))) @@ -210,36 +248,267 @@ describe('future', () => { }) describe('concat', () => { - it('should behave like fulfilled', () => { + it('should be identity for fulfill', () => { const { resolve, promise } = future() - const p1 = fulfill(1) const p2 = fulfill(2) - resolve(p1) - return assertSame(p1.concat(p2), promise.concat(p2)) + assert.strictEqual(p1, promise.concat(p2)) }) - it('should behave like rejected', () => { + it('should be identity for reject', () => { const { resolve, promise } = future() + const p1 = silenced(reject(new Error())) + const p2 = silenced(reject(new Error())) + resolve(p1) + assert.strictEqual(p1, promise.concat(p2)) + }) - const p1 = reject(new Error()) - const p2 = reject(new Error()) - silenceError(p1) - silenceError(p2) - + it('should return other for never', () => { + const { resolve, promise } = future() + const p1 = never() + const p2 = fulfill(2) resolve(p1) assert.strictEqual(p1.concat(p2), promise.concat(p2)) }) + }) + }) + + describe('before being resolved to another promise', () => { + describe('state', () => { + it('should be pending', () => { + const { promise } = future() + assert(isPending(promise)) + }) - it('should behave like never', () => { + it('should not be settled', () => { + const { promise } = future() + assert(!isSettled(promise)) + }) + }) + + describe('inspect', () => { + it('should not be fulfilled', () => { + const { promise } = future() + assert.notStrictEqual(fulfill().inspect(), promise.inspect()) + }) + + it('should not be rejected', () => { + const { promise } = future() + assert.notStrictEqual(silenced(reject()).inspect(), promise.inspect()) + }) + }) + + describe('then', () => { + it('should behave like mapped for fulfill', () => { const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(f) + resolve(p) + return assertSame(p.map(f), res) + }) - const p1 = never() - const p2 = fulfill(2) + it('should behave like chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(fp) + resolve(p) + return assertSame(p.chain(fp), res) + }) - resolve(p1) - return assertSame(p1.concat(p2), promise.concat(p2)) + it('should behave like rejection chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(rp) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejected for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.then(f) + resolve(p) + return assertSame(p, res) + }) + + /* it('should have never state for never', () => { + const { resolve, promise } = future() + const p = never() + const res = promise.then(f) + resolve(p) + assert(isNever(res)) + }) */ + }) + + describe('catch', () => { + it('should behave like fulfilled for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.catch(f) + resolve(p) + return assertSame(p, res) + }) + + it('should behave like mapped for reject', () => { + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(f) + resolve(p) + return assertSame(p.catch(f), res) + }) + + it('should behave like chained for reject', () => { + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(fp) + resolve(p) + return assertSame(p.catch(fp), res) + }) + + it('should behave like rejection chained for reject', () => { + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(rp) + resolve(p) + return assertSame(p.catch(rp), res) + }) + + /* it('should have never state for never', () => { + const { resolve, promise } = future() + const p = never() + const res = promise.catch(f) + resolve(p) + assert(isNever(res)) + }) */ + }) + + describe('map', () => { + it('should behave like mapped for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.map(f) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like rejection for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.map(f) + resolve(p) + return assertSame(p, res) + }) + + /* it('should have never state for never', () => { + const { resolve, promise } = future() + const p = never() + const res = promise.map(f) + resolve(p) + assert(isNever(res)) + }) */ + }) + + describe('ap', () => { + it('should behave like apply for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(f) + const q = fulfill(1) + const res = promise.ap(q) + resolve(p) + return assertSame(p.ap(q), res) + }) + + it('should behave like rejected for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(f)) + const res = promise.ap(fulfill(1)) + resolve(p) + return assertSame(p, res) + }) + + /* it('should have never state for never', () => { + const { resolve, promise } = future() + const p = never() + const res = promise.ap(fulfill(1)) + resolve(p) + assert(isNever(res)) + }) */ + }) + + describe('chain', () => { + it('should behave like chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(fp) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(rp) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejected for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.chain(fp) + resolve(p) + return assertSame(p, res) + }) + + /* it('should have never state for never', () => { + const { resolve, promise } = future() + const p = never() + const res = promise.chain(fp) + resolve(p) + assert(isNever(res)) + }) */ + }) + + describe('concat', () => { + it('should behave like fulfilled other for fulfill', () => { + const { resolve, promise } = future() + const p = fulfill(2) + const res = promise.concat(p) + resolve(fulfill(1)) + return assertSame(p, res) + }) + + it('should behave like rejected other for fulfill', () => { + const { resolve, promise } = future() + const p = silenced(reject(2)) + const res = promise.concat(p) + resolve(fulfill(1)) + return assertSame(p, res) + }) + + it('should behave like fulfilled other for reject', () => { + const { resolve, promise } = future() + const p = fulfill(2) + const res = promise.concat(p) + resolve(silenced(reject(1))) + return assertSame(p, res) + }) + + it('should behave like rejected other for reject', () => { + const { resolve, promise } = future() + const p = silenced(reject(2)) + const res = promise.concat(p) + resolve(silenced(reject(1))) + return assertSame(p, res) + }) + + it('should behave like other for never', () => { + const { resolve, promise } = future() + const p = fulfill(2) + const res = promise.concat(p) + resolve(never()) + return assertSame(p, res) }) }) }) diff --git a/test/lib/test-util.js b/test/lib/test-util.js index f050f15..d1bd76b 100644 --- a/test/lib/test-util.js +++ b/test/lib/test-util.js @@ -3,7 +3,8 @@ import assert from 'assert' export function assertSame (ap, bp) { - return ap.then(a => bp.then(b => assert(a === b))) + return ap.then(a => bp.then(b => assert.strictEqual(a, b)), + a => bp.then(x => { throw x }, b => assert.strictEqual(a, b))) } export function throwingIterable (e) { diff --git a/test/of-test.js b/test/of-test.js index e2bc785..d46d0c0 100644 --- a/test/of-test.js +++ b/test/of-test.js @@ -1,27 +1,27 @@ import { describe, it } from 'mocha' -import { Future, reject } from '../src/Promise' +import { Promise, reject } from '../src/main' import { silenceError, getValue } from '../src/inspect' import assert from 'assert' -describe('fulfill', () => { +describe('of', () => { it('should wrap value', () => { const x = {} - return Future.of(x).then(y => assert(x === y)) + return Promise.of(x).then(y => assert.strictEqual(x, y)) }) it('should be immediately fulfilled', () => { - let x = {} - assert.strictEqual(x, getValue(Future.of(x))) + const x = {} + assert.strictEqual(x, getValue(Promise.of(x))) }) it('should wrap promise', () => { - const x = Future.of({}) - return Future.of(x).then(y => assert(x === y)) + const x = Promise.of({}) + return Promise.of(x).then(y => assert.strictEqual(x, y)) }) it('should wrap rejected promise', () => { const x = reject({}) silenceError(x) - return Future.of(x).then(y => assert(x === y)) + return Promise.of(x).then(y => assert.strictEqual(x, y)) }) }) diff --git a/test/reject-test.js b/test/reject-test.js index 1227dd9..2819d58 100644 --- a/test/reject-test.js +++ b/test/reject-test.js @@ -4,26 +4,26 @@ import { silenceError } from '../src/inspect' import assert from 'assert' describe('reject', () => { - it('then should be identity without f', () => { + it('then should be identity without r callback', () => { const p = reject(true) silenceError(p) assert.strictEqual(p, p.then(assert.ifError)) }) it('map should be identity', () => { - var p = reject(true) + const p = reject(true) silenceError(p) assert.strictEqual(p, p.map(assert.ifError)) }) it('ap should be identity', () => { - var p = reject(assert.ifError) + const p = reject(assert.ifError) silenceError(p) assert.strictEqual(p, p.ap(fulfill(true))) }) it('chain should be identity', () => { - var p = reject() + const p = reject() silenceError(p) assert.strictEqual(p, p.chain(fulfill)) }) diff --git a/test/then-test.js b/test/then-test.js index b3a5d75..0132906 100644 --- a/test/then-test.js +++ b/test/then-test.js @@ -2,7 +2,7 @@ import { describe, it } from 'mocha' import { delay, reject } from '../src/main' import assert from 'assert' -describe('map', function () { +describe('then', function () { it('should not change value when f is not a function', () => { let expected = {} return delay(1, expected).then() From a419afa267802e01a7e594f986e11232e21a7269 Mon Sep 17 00:00:00 2001 From: Bergi Date: Sat, 25 Jun 2016 20:33:50 +0200 Subject: [PATCH 04/13] enable ES6 syntax for profiling just switch "main" of package.json to "dist/creed.node.js" benchmarks will automatically use it --- .gitignore | 1 + package.json | 5 +++-- perf/doxbee-sequential-errors/promises-creed-algebraic.js | 2 +- perf/doxbee-sequential-errors/promises-creed-generator.js | 2 +- perf/doxbee-sequential-errors/promises-creed.js | 2 +- perf/doxbee-sequential/promises-creed-algebraic.js | 2 +- perf/doxbee-sequential/promises-creed-generator.js | 2 +- perf/doxbee-sequential/promises-creed.js | 2 +- perf/lib/fakesP.js | 2 +- perf/madeup-parallel/promises-creed-generator.js | 2 +- perf/madeup-parallel/promises-creed.js | 2 +- perf/performance.js | 3 +-- src/main.js | 4 ++-- 13 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index d022f19..7eaac36 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ experiments/ node_modules/ build/ coverage/ +perf/logs/ diff --git a/package.json b/package.json index 0a7d9e3..83acb08 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "jsnext:main": "dist/creed.es.js", "files": [ "dist/creed.js", - "dist/creed.es.js" + "dist/creed.es.js", + "dist/creed.node.js" ], "repository": { "type": "git", @@ -28,7 +29,7 @@ "scripts": { "compile": "npm run compile-src", "compile-src": "mkdirp build/src && buble -m -i src -o build/src --no modules", - "build-dist": "npm run compile && mkdirp dist && rollup -c", + "build-dist": "npm run compile && mkdirp dist && rollup -c && rollup -f cjs -o dist/creed.node.js src/main.js", "build": "npm run build-dist && uglifyjs -c \"warnings=false\" -m -o dist/creed.min.js -- dist/creed.js", "preversion": "npm run build", "check-coverage": "istanbul check-coverage --statements 100 --branches 100 --lines 100 --functions 100 coverage/coverage*.json", diff --git a/perf/doxbee-sequential-errors/promises-creed-algebraic.js b/perf/doxbee-sequential-errors/promises-creed-algebraic.js index 8df2c47..736ad7f 100644 --- a/perf/doxbee-sequential-errors/promises-creed-algebraic.js +++ b/perf/doxbee-sequential-errors/promises-creed-algebraic.js @@ -2,7 +2,7 @@ global.useCreed = true; global.useQ = false; global.useBluebird = false; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/doxbee-sequential-errors/promises-creed-generator.js b/perf/doxbee-sequential-errors/promises-creed-generator.js index 6eac22f..8ee5607 100644 --- a/perf/doxbee-sequential-errors/promises-creed-generator.js +++ b/perf/doxbee-sequential-errors/promises-creed-generator.js @@ -2,7 +2,7 @@ global.useCreed = true; global.useQ = false; global.useBluebird = false; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/doxbee-sequential-errors/promises-creed.js b/perf/doxbee-sequential-errors/promises-creed.js index dce1b70..733e745 100644 --- a/perf/doxbee-sequential-errors/promises-creed.js +++ b/perf/doxbee-sequential-errors/promises-creed.js @@ -2,7 +2,7 @@ global.useCreed = true; global.useQ = false; global.useBluebird = false; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/doxbee-sequential/promises-creed-algebraic.js b/perf/doxbee-sequential/promises-creed-algebraic.js index 94c664d..74b5a95 100644 --- a/perf/doxbee-sequential/promises-creed-algebraic.js +++ b/perf/doxbee-sequential/promises-creed-algebraic.js @@ -2,7 +2,7 @@ global.useCreed = true; global.useQ = false; global.useBluebird = false; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/doxbee-sequential/promises-creed-generator.js b/perf/doxbee-sequential/promises-creed-generator.js index d594b87..5ed6323 100644 --- a/perf/doxbee-sequential/promises-creed-generator.js +++ b/perf/doxbee-sequential/promises-creed-generator.js @@ -1,7 +1,7 @@ global.useBluebird = false; global.useQ = false; global.useCreed = true; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); module.exports = creed.coroutine(function* upload(stream, idOrPath, tag, done) { diff --git a/perf/doxbee-sequential/promises-creed.js b/perf/doxbee-sequential/promises-creed.js index d582818..c0bf159 100644 --- a/perf/doxbee-sequential/promises-creed.js +++ b/perf/doxbee-sequential/promises-creed.js @@ -2,7 +2,7 @@ global.useCreed = true; global.useQ = false; global.useBluebird = false; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/lib/fakesP.js b/perf/lib/fakesP.js index 9fd13d6..7e8d3ff 100644 --- a/perf/lib/fakesP.js +++ b/perf/lib/fakesP.js @@ -83,7 +83,7 @@ else if (global.useNative) { }; } else if (global.useCreed) { - var lifter = require('../../dist/creed').fromNode; + var lifter = require('../..').fromNode; } else { var lifter = require('when/node').lift; diff --git a/perf/madeup-parallel/promises-creed-generator.js b/perf/madeup-parallel/promises-creed-generator.js index 7dd5ba2..4ce86f7 100644 --- a/perf/madeup-parallel/promises-creed-generator.js +++ b/perf/madeup-parallel/promises-creed-generator.js @@ -3,7 +3,7 @@ global.useQ = false; global.useWhen = false; global.useCreed = true; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); module.exports = creed.coroutine(function* upload(stream, idOrPath, tag, done) { diff --git a/perf/madeup-parallel/promises-creed.js b/perf/madeup-parallel/promises-creed.js index 301ca5d..c8487e0 100644 --- a/perf/madeup-parallel/promises-creed.js +++ b/perf/madeup-parallel/promises-creed.js @@ -4,7 +4,7 @@ global.useWhen = false; global.useCreed = true; -var creed = require('../../dist/creed'); +var creed = require('../..'); require('../lib/fakesP'); diff --git a/perf/performance.js b/perf/performance.js index 8d42ce8..0bca2b6 100644 --- a/perf/performance.js +++ b/perf/performance.js @@ -1,6 +1,5 @@ var args = require('optimist').argv; - var path = require('path'); global.LIKELIHOOD_OF_REJECTION = args.e || 0.1; @@ -138,7 +137,7 @@ function measure(files, requests, time, parg, callback) { async.mapSeries(files, function(f, done) { console.log("benchmarking", f); var logFile = path.basename(f) + ".log"; - var profileFlags = ["--prof", "--logfile=C:/etc/v8/" + logFile]; + var profileFlags = ["--prof", "--logfile=logs/" + logFile]; var argsFork = [__filename, '--n', requests, diff --git a/src/main.js b/src/main.js index afee964..e4b0b86 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,7 @@ export { // coroutine :: Generator e a -> (...* -> Promise e a) // Make a coroutine from a promise-yielding generator export function coroutine (generator) { - return function (...args) { + return function coroutinified (...args) { return runGenerator(generator, this, args) } } @@ -50,7 +50,7 @@ function runGenerator (generator, thisArg, args) { // fromNode :: NodeApi e a -> (...args -> Promise e a) // Turn a Node API into a promise API export function fromNode (f) { - return function (...args) { + return function promisified (...args) { return runResolver(_runNode, f, this, args, new Future()) } } From 130aed74ccc3a97fdfc1c54dd0071ff29a3c9eaf Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 1 Jul 2016 18:43:41 +0200 Subject: [PATCH 05/13] better module structure * No more dependency injection of `fulfill`/`resolve` functions * moved around code a bit, especially utils, iteration stuff, combinator functions * simplified some exports that now export a function, not a function factory * Some circular dependencies, mostly completely declarative - Promise - ErrorHandler, inspect: silenceError - Promise - combinators: Future, never, silenceError / race * tests always import `main` unless they test internals --- src/Any.js | 2 +- src/ErrorHandler.js | 36 ++++++++------- src/Promise.js | 92 ++++++++++++------------------------- src/Race.js | 8 ++-- src/Settle.js | 7 ++- src/TaskQueue.js | 14 +++++- src/async.js | 10 ++-- src/chain.js | 6 +-- src/combinators.js | 75 ++++++++++++++++++++++++++++++ src/coroutine.js | 12 ++--- src/delay.js | 2 +- src/emitError.js | 77 +++++++++++++++---------------- src/inspect.js | 12 +---- src/iterable.js | 36 ++++++++++----- src/main.js | 96 ++++++++------------------------------- src/map.js | 2 +- src/maybeThenable.js | 4 -- src/timeout.js | 2 +- src/util.js | 7 +++ test/ErrorHandler-test.js | 10 ++-- test/TaskQueue-test.js | 2 +- test/all-test.js | 3 +- test/concat-test.js | 2 +- test/coroutine-test.js | 2 +- test/delay-test.js | 5 +- test/empty-test.js | 5 +- test/fulfill-test.js | 4 +- test/future-test.js | 3 +- test/inspect-test.js | 5 +- test/iterable-test.js | 6 +-- test/of-test.js | 4 +- test/race-test.js | 3 +- test/reject-test.js | 4 +- test/resolve-test.js | 3 +- test/settle-test.js | 3 +- test/timeout-test.js | 5 +- test/toString-test.js | 4 +- 37 files changed, 284 insertions(+), 289 deletions(-) create mode 100644 src/combinators.js delete mode 100644 src/maybeThenable.js create mode 100644 src/util.js diff --git a/src/Any.js b/src/Any.js index be84b2e..05f8423 100644 --- a/src/Any.js +++ b/src/Any.js @@ -1,4 +1,4 @@ -import { silenceError } from './inspect.js' +import { silenceError } from './Promise' // deferred export default class Any { constructor () { diff --git a/src/ErrorHandler.js b/src/ErrorHandler.js index b4e0607..a53247f 100644 --- a/src/ErrorHandler.js +++ b/src/ErrorHandler.js @@ -1,20 +1,22 @@ -import { silenceError, isHandled } from './inspect' +import { silenceError } from './Promise' // deferred +import { isHandled } from './inspect' -const UNHANDLED_REJECTION = 'unhandledRejection' -const HANDLED_REJECTION = 'rejectionHandled' +export const UNHANDLED_REJECTION = 'unhandledRejection' +export const HANDLED_REJECTION = 'rejectionHandled' export default class ErrorHandler { constructor (emitEvent, reportError) { this.errors = [] this.emit = emitEvent this.reportError = reportError + this.report = () => this._reportErrors() } track (e) { if (!this.emit(UNHANDLED_REJECTION, e, e.value)) { /* istanbul ignore else */ if (this.errors.length === 0) { - setTimeout(reportErrors, 1, this.reportError, this.errors) + setTimeout(this.report, 1) } this.errors.push(e) } @@ -24,22 +26,22 @@ export default class ErrorHandler { silenceError(e) this.emit(HANDLED_REJECTION, e) } -} -function reportErrors (report, errors) { - try { - reportAll(errors, report) - } finally { - errors.length = 0 + _reportErrors () { + try { + this._reportAll(this.errors) + } finally { + this.errors.length = 0 + } } -} -function reportAll (errors, report) { - for (let i = 0; i < errors.length; ++i) { - const e = errors[i] - /* istanbul ignore else */ - if (!isHandled(e)) { - report(e) + _reportAll (errors) { + for (let i = 0; i < errors.length; ++i) { + const e = errors[i] + /* istanbul ignore else */ + if (!isHandled(e)) { + this.reportError(e) + } } } } diff --git a/src/Promise.js b/src/Promise.js index f6299a4..25ca95f 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -1,23 +1,22 @@ -import TaskQueue from './TaskQueue' -import ErrorHandler from './ErrorHandler' -import makeEmitError from './emitError' -import maybeThenable from './maybeThenable' -import { PENDING, FULFILLED, REJECTED, NEVER } from './state' +import { isObject } from './util' +import { PENDING, FULFILLED, REJECTED, NEVER, HANDLED } from './state' import { isNever, isSettled } from './inspect' +import { TaskQueue, Continuation } from './TaskQueue' +import ErrorHandler from './ErrorHandler' +import emitError from './emitError' + +import Action from './Action' import then from './then' import map from './map' import chain from './chain' -import Race from './Race' -import Merge from './Merge' -import { resolveIterable, resultsArray } from './iterable' +import { race } from './combinators' -const taskQueue = new TaskQueue() -export { taskQueue } +export const taskQueue = new TaskQueue() /* istanbul ignore next */ -const errorHandler = new ErrorHandler(makeEmitError(), e => { +const errorHandler = new ErrorHandler(emitError, e => { throw e.value }) @@ -249,7 +248,7 @@ class Rejected extends Core { constructor (e) { super() this.value = e - this._state = REJECTED + this._state = REJECTED // mutated by the silencer errorHandler.track(this) } @@ -354,6 +353,16 @@ class Never extends Core { } } +const silencer = new Action(never()) +silencer.fulfilled = function fulfilled (p) { } +silencer.rejected = function setHandled (p) { + p._state |= HANDLED +} + +export function silenceError (p) { + p._runAction(silencer) +} + // ------------------------------------------------------------- // ## Creating promises // ------------------------------------------------------------- @@ -362,10 +371,14 @@ class Never extends Core { // resolve :: a -> Promise e a export function resolve (x) { return isPromise(x) ? x.near() - : maybeThenable(x) ? refForMaybeThenable(fulfill, x) + : isObject(x) ? refForMaybeThenable(x) : new Fulfilled(x) } +export function resolveObject (o) { + return isPromise(o) ? o.near() : refForMaybeThenable(o) +} + // reject :: e -> Promise e a export function reject (e) { return new Rejected(e) @@ -388,40 +401,6 @@ export function future () { return {resolve: x => promise._resolve(x), promise} } -// ------------------------------------------------------------- -// ## Iterables -// ------------------------------------------------------------- - -// all :: Iterable (Promise e a) -> Promise e [a] -export function all (promises) { - const handler = new Merge(allHandler, resultsArray(promises)) - return iterablePromise(handler, promises) -} - -const allHandler = { - merge (promise, args) { - promise._fulfill(args) - } -} - -// race :: Iterable (Promise e a) -> Promise e a -export function race (promises) { - return iterablePromise(new Race(never), promises) -} - -function isIterable (x) { - return typeof x === 'object' && x !== null -} - -export function iterablePromise (handler, iterable) { - if (!isIterable(iterable)) { - return reject(new TypeError('expected an iterable')) - } - - const p = new Future() - return resolveIterable(resolveMaybeThenable, handler, iterable, p) -} - // ------------------------------------------------------------- // # Internals // ------------------------------------------------------------- @@ -431,16 +410,12 @@ function isPromise (x) { return x instanceof Core } -function resolveMaybeThenable (x) { - return isPromise(x) ? x.near() : refForMaybeThenable(fulfill, x) -} - -function refForMaybeThenable (otherwise, x) { +function refForMaybeThenable (x) { try { const then = x.then return typeof then === 'function' ? extractThenable(then, x) - : otherwise(x) + : fulfill(x) } catch (e) { return new Rejected(e) } @@ -462,14 +437,3 @@ function extractThenable (thn, thenable) { function cycle () { return new Rejected(new TypeError('resolution cycle')) } - -class Continuation { - constructor (action, promise) { - this.action = action - this.promise = promise - } - - run () { - this.promise._runAction(this.action) - } -} diff --git a/src/Race.js b/src/Race.js index 9718298..c9610d6 100644 --- a/src/Race.js +++ b/src/Race.js @@ -1,8 +1,6 @@ -export default class Race { - constructor (never) { - this.never = never - } +import { never } from './Promise' // deferred +export default class Race { valueAt (x, i, promise) { promise._fulfill(x) } @@ -17,7 +15,7 @@ export default class Race { complete (total, promise) { if (total === 0) { - promise._become(this.never()) + promise._become(never()) } } } diff --git a/src/Settle.js b/src/Settle.js index 789c3d6..e2aa930 100644 --- a/src/Settle.js +++ b/src/Settle.js @@ -1,14 +1,13 @@ -import { silenceError } from './inspect' +import { fulfill, silenceError } from './Promise' // deferred export default class Settle { - constructor (resolve, results) { + constructor (results) { this.pending = 0 this.results = results - this.resolve = resolve } valueAt (x, i, promise) { - this.settleAt(this.resolve(x), i, promise) + this.settleAt(fulfill(x), i, promise) } fulfillAt (p, i, promise) { diff --git a/src/TaskQueue.js b/src/TaskQueue.js index 14dd40e..d880b7a 100644 --- a/src/TaskQueue.js +++ b/src/TaskQueue.js @@ -1,6 +1,6 @@ import makeAsync from './async' -export default class TaskQueue { +export class TaskQueue { constructor () { this.tasks = new Array(2 << 15) this.length = 0 @@ -24,3 +24,15 @@ export default class TaskQueue { this.length = 0 } } + +// make an Action runnable on a Promise +export class Continuation { + constructor (action, promise) { + this.action = action + this.promise = promise + } + + run () { + this.promise._runAction(this.action) + } +} diff --git a/src/async.js b/src/async.js index edc4c2b..4aa4b51 100644 --- a/src/async.js +++ b/src/async.js @@ -2,11 +2,11 @@ import { isNode, MutationObs } from './env' /* global process,document */ -export default function (f) { - return isNode ? createNodeScheduler(f) /* istanbul ignore next */ - : MutationObs ? createBrowserScheduler(f) - : createFallbackScheduler(f) -} +const createScheduler = isNode ? createNodeScheduler /* istanbul ignore next */ + : MutationObs ? createBrowserScheduler + : createFallbackScheduler + +export { createScheduler as default } /* istanbul ignore next */ function createFallbackScheduler (f) { diff --git a/src/chain.js b/src/chain.js index ae46bcf..5ce7bf1 100644 --- a/src/chain.js +++ b/src/chain.js @@ -1,7 +1,7 @@ +import { isObject } from './util' import Action from './Action' -import maybeThenable from './maybeThenable' -export default function (f, p, promise) { +export default function chain (f, p, promise) { p._when(new Chain(f, promise)) return promise } @@ -17,7 +17,7 @@ class Chain extends Action { } handle (y) { - if (!(maybeThenable(y) && typeof y.then === 'function')) { + if (!(isObject(y) && typeof y.then === 'function')) { this.promise._reject(new TypeError('f must return a promise')) } diff --git a/src/combinators.js b/src/combinators.js new file mode 100644 index 0000000..859a10a --- /dev/null +++ b/src/combinators.js @@ -0,0 +1,75 @@ +import { taskQueue } from './Promise' // deferred +import { iterablePromise, resultsArray } from './iterable' +import Race from './Race' +import Any from './Any' +import Merge from './Merge' +import Settle from './Settle' + +// ------------------------------------------------------------- +// ## Iterables +// ------------------------------------------------------------- + +// all :: Iterable (Promise e a) -> Promise e [a] +export function all (promises) { + const handler = new Merge(allHandler, resultsArray(promises)) + return iterablePromise(handler, promises) +} + +const allHandler = { + merge (promise, args) { + promise._fulfill(args) + } +} + +// race :: Iterable (Promise e a) -> Promise e a +export function race (promises) { + return iterablePromise(new Race(), promises) +} + +// any :: Iterable (Promise e a) -> Promise e a +export function any (promises) { + return iterablePromise(new Any(), promises) +} + +// settle :: Iterable (Promise e a) -> Promise e [Promise e a] +export function settle (promises) { + const handler = new Settle(resultsArray(promises)) + return iterablePromise(handler, promises) +} + +// ------------------------------------------------------------- +// ## Lifting +// ------------------------------------------------------------- + +// merge :: (...* -> b) -> ...Promise e a -> Promise e b +export function merge (f, ...args) { + return runMerge(f, this, args) +} + +function runMerge (f, thisArg, args) { + const handler = new Merge(new MergeHandler(f, thisArg), resultsArray(args)) + return iterablePromise(handler, args) +} + +class MergeHandler { + constructor (f, c) { + this.f = f + this.c = c + this.promise = void 0 + this.args = void 0 + } + + merge (promise, args) { + this.promise = promise + this.args = args + taskQueue.add(this) + } + + run () { + try { + this.promise._resolve(this.f.apply(this.c, this.args)) + } catch (e) { + this.promise._reject(e) + } + } +} diff --git a/src/coroutine.js b/src/coroutine.js index 098e262..e9b9be2 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -1,15 +1,15 @@ +import { resolve } from './Promise' import Action from './Action' -export default function (resolve, iterator, promise) { - new Coroutine(resolve, iterator, promise).run() - // taskQueue.add(new Coroutine(resolve, iterator, promise)) +export default function coroutine (iterator, promise) { + new Coroutine(iterator, promise).run() + // taskQueue.add(new Coroutine(iterator, promise)) return promise } class Coroutine extends Action { - constructor (resolve, iterator, promise) { + constructor (iterator, promise) { super(promise) - this.resolve = resolve this.next = iterator.next.bind(iterator) this.throw = iterator.throw.bind(iterator) } @@ -23,7 +23,7 @@ class Coroutine extends Action { return this.promise._resolve(result.value) } - this.resolve(result.value)._runAction(this) + resolve(result.value)._runAction(this) } fulfilled (ref) { diff --git a/src/delay.js b/src/delay.js index cef6765..aaad5bf 100644 --- a/src/delay.js +++ b/src/delay.js @@ -1,6 +1,6 @@ import Action from './Action' -export default function (ms, p, promise) { +export default function delay (ms, p, promise) { p._runAction(new Delay(ms, promise)) return promise } diff --git a/src/emitError.js b/src/emitError.js index 9542313..a23c758 100644 --- a/src/emitError.js +++ b/src/emitError.js @@ -1,47 +1,44 @@ import { isNode } from './env' +import { noop } from './util' +import { UNHANDLED_REJECTION } from './ErrorHandler' -const UNHANDLED_REJECTION = 'unhandledRejection' - -export default function () { - /*global process, self, CustomEvent*/ - // istanbul ignore else */ - if (isNode && typeof process.emit === 'function') { - // Returning falsy here means to call the default reportRejection API. - // This is safe even in browserify since process.emit always returns - // falsy in browserify: - // https://github.com/defunctzombie/node-process/blob/master/browser.js#L40-L46 - return function (type, error) { - return type === UNHANDLED_REJECTION - ? process.emit(type, error.value, error) - : process.emit(type, error) +let emitError +/*global process, self, CustomEvent*/ +// istanbul ignore else */ +if (isNode && typeof process.emit === 'function') { + // Returning falsy here means to call the default reportRejection API. + // This is safe even in browserify since process.emit always returns + // falsy in browserify: + // https://github.com/defunctzombie/node-process/blob/master/browser.js#L40-L46 + emitError = function emit (type, error) { + return type === UNHANDLED_REJECTION + ? process.emit(type, error.value, error) + : process.emit(type, error) + } +} else if (typeof self !== 'undefined' && typeof CustomEvent === 'function') { + emitError = (function (self, CustomEvent) { + try { + let usable = new CustomEvent(UNHANDLED_REJECTION) instanceof CustomEvent + if (!usable) return noop + } catch (e) { + return noop } - } else if (typeof self !== 'undefined' && typeof CustomEvent === 'function') { - return (function (noop, self, CustomEvent) { - var hasCustomEvent - try { - hasCustomEvent = new CustomEvent(UNHANDLED_REJECTION) instanceof CustomEvent - } catch (e) { - hasCustomEvent = false - } - return !hasCustomEvent ? noop : function (type, error) { - const ev = new CustomEvent(type, { - detail: { - reason: error.value, - promise: error - }, - bubbles: false, - cancelable: true - }) + return function emit (type, error) { + const ev = new CustomEvent(type, { + detail: { + reason: error.value, + promise: error + }, + bubbles: false, + cancelable: true + }) - return !self.dispatchEvent(ev) - } - }(noop, self, CustomEvent)) - } - - // istanbul ignore next */ - return noop + return !self.dispatchEvent(ev) + } + }(self, CustomEvent)) +} else { + emitError = noop } -// istanbul ignore next */ -function noop () {} +export default emitError diff --git a/src/inspect.js b/src/inspect.js index bfe767d..5eaad63 100644 --- a/src/inspect.js +++ b/src/inspect.js @@ -1,5 +1,5 @@ import { PENDING, FULFILLED, REJECTED, SETTLED, NEVER, HANDLED } from './state' -import Action from './Action' +import { silenceError } from './Promise' // deferred export function isPending (p) { return (p.state() & PENDING) > 0 @@ -43,13 +43,3 @@ export function getReason (p) { silenceError(n) return n.value } - -export function silenceError (p) { - p._runAction(silencer) -} - -const silencer = new Action(null) -silencer.fulfilled = function fulfilled (p) { } -silencer.rejected = function setHandled (p) { - p._state |= HANDLED -} diff --git a/src/iterable.js b/src/iterable.js index a9d3c8b..07b4efe 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -1,32 +1,46 @@ -import { isFulfilled, isRejected, silenceError } from './inspect' +import { isObject } from './util' +import { Future, reject, resolveObject, silenceError } from './Promise' // deferred +import { isFulfilled, isRejected } from './inspect' import Action from './Action' -import maybeThenable from './maybeThenable' + +function isIterable (x) { + return typeof x === 'object' && x !== null +} + +export function iterablePromise (handler, iterable) { + if (!isIterable(iterable)) { + return reject(new TypeError('expected an iterable')) + } + + const p = new Future() + return resolveIterable(handler, iterable, p) +} export function resultsArray (iterable) { return Array.isArray(iterable) ? new Array(iterable.length) : [] } -export function resolveIterable (resolve, handler, promises, promise) { +export function resolveIterable (handler, promises, promise) { const run = Array.isArray(promises) ? runArray : runIterable try { - run(resolve, handler, promises, promise) + run(handler, promises, promise) } catch (e) { promise._reject(e) } return promise.near() } -function runArray (resolve, handler, promises, promise) { +function runArray (handler, promises, promise) { let i = 0 for (; i < promises.length; ++i) { - handleItem(resolve, handler, promises[i], i, promise) + handleItem(handler, promises[i], i, promise) } handler.complete(i, promise) } -function runIterable (resolve, handler, promises, promise) { +function runIterable (handler, promises, promise) { let i = 0 const iter = promises[Symbol.iterator]() @@ -35,20 +49,20 @@ function runIterable (resolve, handler, promises, promise) { if (step.done) { break } - handleItem(resolve, handler, step.value, i++, promise) + handleItem(handler, step.value, i++, promise) } handler.complete(i, promise) } -function handleItem (resolve, handler, x, i, promise) { +function handleItem (handler, x, i, promise) { /*eslint complexity:[1,6]*/ - if (!maybeThenable(x)) { + if (!isObject(x)) { handler.valueAt(x, i, promise) return } - const p = resolve(x) + const p = resolveObject(x) if (promise._isResolved()) { if (!isFulfilled(p)) { diff --git a/src/main.js b/src/main.js index e4b0b86..c7f3ceb 100644 --- a/src/main.js +++ b/src/main.js @@ -1,28 +1,22 @@ -import { isFulfilled, isRejected, isSettled, isPending, isNever, getValue, getReason } from './inspect' -import { Future, resolve, reject, future, never, fulfill, all, race, iterablePromise, taskQueue } from './Promise' +// ------------------------------------------------------------- +// ## Core promise methods +// ------------------------------------------------------------- + +/* eslint-disable no-duplicate-imports */ +export { resolve, reject, future, never, fulfill } from './Promise' +import { Future, resolve, reject } from './Promise' +export { isFulfilled, isRejected, isSettled, isPending, isNever, getValue, getReason } from './inspect' +import { isRejected, isSettled, isNever } from './inspect' +export { all, race, any, settle, merge } from './combinators' +import { all, race } from './combinators' import _delay from './delay' import _timeout from './timeout' -import Any from './Any' -import Merge from './Merge' -import Settle from './Settle' -import { resultsArray } from './iterable' - import _runPromise from './runPromise' import _runNode from './node' import _runCoroutine from './coroutine.js' -// ------------------------------------------------------------- -// ## Core promise methods -// ------------------------------------------------------------- - -export { - resolve, reject, future, never, fulfill, all, race, - isFulfilled, isRejected, isSettled, isPending, isNever, - getValue, getReason -} - // ------------------------------------------------------------- // ## Coroutine // ------------------------------------------------------------- @@ -37,7 +31,7 @@ export function coroutine (generator) { function runGenerator (generator, thisArg, args) { const iterator = generator.apply(thisArg, args) - return _runCoroutine(resolve, iterator, new Future()) + return _runCoroutine(iterator, new Future()) } // ------------------------------------------------------------- @@ -85,6 +79,12 @@ function runResolver (run, f, thisArg, args, p) { return p } +function checkFunction (f) { + if (typeof f !== 'function') { + throw new TypeError('must provide a resolver function') + } +} + // ------------------------------------------------------------- // ## Time // ------------------------------------------------------------- @@ -103,64 +103,6 @@ export function timeout (ms, x) { return isSettled(p) ? p : _timeout(ms, p, new Future()) } -// ------------------------------------------------------------- -// ## Iterables -// ------------------------------------------------------------- - -// any :: Iterable (Promise e a) -> Promise e a -export function any (promises) { - return iterablePromise(new Any(), promises) -} - -// settle :: Iterable (Promise e a) -> Promise e [Promise e a] -export function settle (promises) { - const handler = new Settle(resolve, resultsArray(promises)) - return iterablePromise(handler, promises) -} - -// ------------------------------------------------------------- -// ## Lifting -// ------------------------------------------------------------- - -// merge :: (...* -> b) -> ...Promise e a -> Promise e b -export function merge (f, ...args) { - return runMerge(f, this, args) -} - -function runMerge (f, thisArg, args) { - const handler = new Merge(new MergeHandler(f, thisArg), resultsArray(args)) - return iterablePromise(handler, args) -} - -class MergeHandler { - constructor (f, c) { - this.f = f - this.c = c - this.promise = void 0 - this.args = void 0 - } - - merge (promise, args) { - this.promise = promise - this.args = args - taskQueue.add(this) - } - - run () { - try { - this.promise._resolve(this.f.apply(this.c, this.args)) - } catch (e) { - this.promise._reject(e) - } - } -} - -function checkFunction (f) { - if (typeof f !== 'function') { - throw new TypeError('must provide a resolver function') - } -} - // ------------------------------------------------------------- // ## ES6 Promise polyfill // ------------------------------------------------------------- @@ -169,7 +111,7 @@ const NOARGS = [] // type Resolve a = a -> () // type Reject e = e -> () -// Promise :: (Resolve a -> Reject e) -> Promise e a +// Promise :: (Resolve a -> Reject e -> ()) -> Promise e a class CreedPromise extends Future { constructor (f) { super() diff --git a/src/map.js b/src/map.js index 8ec6f6b..11f29d5 100644 --- a/src/map.js +++ b/src/map.js @@ -1,6 +1,6 @@ import Action from './Action' -export default function (f, p, promise) { +export default function map (f, p, promise) { p._when(new Map(f, promise)) return promise } diff --git a/src/maybeThenable.js b/src/maybeThenable.js deleted file mode 100644 index 396d8e4..0000000 --- a/src/maybeThenable.js +++ /dev/null @@ -1,4 +0,0 @@ -// maybeThenable :: * -> boolean -export default function maybeThenable (x) { - return (typeof x === 'object' || typeof x === 'function') && x !== null -} diff --git a/src/timeout.js b/src/timeout.js index 75b8e66..f1094f2 100644 --- a/src/timeout.js +++ b/src/timeout.js @@ -1,7 +1,7 @@ import Action from './Action' import TimeoutError from './TimeoutError' -export default function (ms, p, promise) { +export default function timeout (ms, p, promise) { const timer = setTimeout(rejectOnTimeout, ms, promise) p._runAction(new Timeout(timer, promise)) return promise diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..550c0f7 --- /dev/null +++ b/src/util.js @@ -0,0 +1,7 @@ +// isObject :: * -> boolean +export function isObject (x) { + return (typeof x === 'object' || typeof x === 'function') && x !== null +} + +/* istanbul ignore next */ +export function noop () {} diff --git a/test/ErrorHandler-test.js b/test/ErrorHandler-test.js index 1e224ab..b535013 100644 --- a/test/ErrorHandler-test.js +++ b/test/ErrorHandler-test.js @@ -1,14 +1,16 @@ import { describe, it } from 'mocha' +import '../src/Promise' +import { isHandled } from '../src/inspect' import ErrorHandler from '../src/ErrorHandler' import assert from 'assert' -import { HANDLED } from '../src/state' function fakeError (value) { return { - value: value, + value, _state: 0, + near () { return this }, state () { return this._state }, - _runAction () { this._state |= HANDLED } + _runAction (a) { a.rejected(this) } } } @@ -73,7 +75,7 @@ describe('ErrorHandler', () => { const eh = new ErrorHandler(() => true, fail) eh.untrack(expected) - assert.equal(expected.state(), HANDLED) + assert(isHandled(expected)) }) }) }) diff --git a/test/TaskQueue-test.js b/test/TaskQueue-test.js index 097fba1..c39f404 100644 --- a/test/TaskQueue-test.js +++ b/test/TaskQueue-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import TaskQueue from '../src/TaskQueue' +import { TaskQueue } from '../src/TaskQueue' import assert from 'assert' describe('TaskQueue', () => { diff --git a/test/all-test.js b/test/all-test.js index ffd5c21..f377281 100644 --- a/test/all-test.js +++ b/test/all-test.js @@ -1,5 +1,6 @@ import { describe, it } from 'mocha' -import { Future, all, resolve } from '../src/Promise' +import { all, resolve } from '../src/main' +import { Future } from '../src/Promise' import { throwingIterable, arrayIterable } from './lib/test-util' import assert from 'assert' diff --git a/test/concat-test.js b/test/concat-test.js index dc22460..14853a7 100644 --- a/test/concat-test.js +++ b/test/concat-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' import { fulfill, delay, reject, never } from '../src/main' -import { silenceError } from '../src/inspect' +import { silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' diff --git a/test/coroutine-test.js b/test/coroutine-test.js index cd88b5e..bae18d5 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { fulfill, reject, delay, coroutine } from '../src/main' +import { coroutine, fulfill, reject, delay } from '../src/main' import assert from 'assert' describe('coroutine', function () { diff --git a/test/delay-test.js b/test/delay-test.js index 52770be..84ba2ec 100644 --- a/test/delay-test.js +++ b/test/delay-test.js @@ -1,7 +1,6 @@ import { describe, it } from 'mocha' -import { delay } from '../src/main' -import { Future, never, reject, fulfill } from '../src/Promise' -import { silenceError, isNever, isPending } from '../src/inspect' +import { delay, never, reject, fulfill, isNever, isPending } from '../src/main' +import { Future, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' diff --git a/test/empty-test.js b/test/empty-test.js index 107b2c0..0508829 100644 --- a/test/empty-test.js +++ b/test/empty-test.js @@ -1,10 +1,9 @@ import { describe, it } from 'mocha' -import { Future } from '../src/Promise' -import { isNever } from '../src/inspect' +import { Promise, isNever } from '../src/main' import assert from 'assert' describe('empty', function () { it('should return never', () => { - assert(isNever(Future.empty())) + assert(isNever(Promise.empty())) }) }) diff --git a/test/fulfill-test.js b/test/fulfill-test.js index c6342a2..65290e4 100644 --- a/test/fulfill-test.js +++ b/test/fulfill-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { fulfill, reject } from '../src/main' -import { silenceError, getValue } from '../src/inspect' +import { fulfill, reject, getValue } from '../src/main' +import { silenceError } from '../src/Promise' import assert from 'assert' describe('fulfill', () => { diff --git a/test/future-test.js b/test/future-test.js index 55a5470..158b2fd 100644 --- a/test/future-test.js +++ b/test/future-test.js @@ -1,7 +1,6 @@ import { describe, it } from 'mocha' import { future, reject, fulfill, isSettled, isPending, never } from '../src/main' -import { Future } from '../src/Promise' -import { silenceError } from '../src/inspect' +import { Future, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' diff --git a/test/inspect-test.js b/test/inspect-test.js index c7af240..4a084dd 100644 --- a/test/inspect-test.js +++ b/test/inspect-test.js @@ -1,6 +1,7 @@ import { describe, it } from 'mocha' -import { isFulfilled, isRejected, isSettled, isPending, isHandled, isNever, silenceError, getValue, getReason } from '../src/inspect' -import { resolve, reject, fulfill, never, Future } from '../src/Promise' +import { resolve, reject, fulfill, never } from '../src/main' +import { isFulfilled, isRejected, isSettled, isPending, isNever, getValue, getReason, isHandled } from '../src/inspect' +import { Future, silenceError } from '../src/Promise' import assert from 'assert' describe('inspect', () => { diff --git a/test/iterable-test.js b/test/iterable-test.js index 083f9b9..4479347 100644 --- a/test/iterable-test.js +++ b/test/iterable-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { Future, resolve } from '../src/Promise' import { resolveIterable } from '../src/iterable' +import { Future } from '../src/Promise' import { arrayIterable } from './lib/test-util' import assert from 'assert' @@ -14,7 +14,7 @@ describe('iterable', () => { } const iterable = arrayIterable([1, 2, 3]) - return resolveIterable(resolve, itemHandler, iterable, new Future()) + return resolveIterable(itemHandler, iterable, new Future()) .then(assert.ifError, e => assert.strictEqual(error, e)) }) @@ -31,7 +31,7 @@ describe('iterable', () => { const promise = new Future() promise._resolve(expected) - return resolveIterable(resolve, itemHandler, iterable, promise) + return resolveIterable(itemHandler, iterable, promise) .then(x => assert.strictEqual(expected, x)) }) }) diff --git a/test/of-test.js b/test/of-test.js index d46d0c0..c2c9098 100644 --- a/test/of-test.js +++ b/test/of-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { Promise, reject } from '../src/main' -import { silenceError, getValue } from '../src/inspect' +import { Promise, reject, getValue } from '../src/main' +import { silenceError } from '../src/Promise' import assert from 'assert' describe('of', () => { diff --git a/test/race-test.js b/test/race-test.js index 55448c8..4448e04 100644 --- a/test/race-test.js +++ b/test/race-test.js @@ -1,6 +1,5 @@ import { describe, it } from 'mocha' -import { race, resolve, reject, never } from '../src/main' -import { isNever } from '../src/inspect' +import { race, resolve, reject, never, isNever } from '../src/main' import { throwingIterable } from './lib/test-util' import assert from 'assert' diff --git a/test/reject-test.js b/test/reject-test.js index 2819d58..9f0426b 100644 --- a/test/reject-test.js +++ b/test/reject-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { fulfill, reject } from '../src/main' -import { silenceError } from '../src/inspect' +import { reject, fulfill } from '../src/main' +import { silenceError } from '../src/Promise' import assert from 'assert' describe('reject', () => { diff --git a/test/resolve-test.js b/test/resolve-test.js index 8ae0e13..addfac5 100644 --- a/test/resolve-test.js +++ b/test/resolve-test.js @@ -1,5 +1,6 @@ import { describe, it } from 'mocha' -import { resolve, Future } from '../src/Promise' +import { resolve } from '../src/main' +import { Future } from '../src/Promise' import assert from 'assert' describe('resolve', () => { diff --git a/test/settle-test.js b/test/settle-test.js index 5ae8d2f..797258d 100644 --- a/test/settle-test.js +++ b/test/settle-test.js @@ -1,6 +1,5 @@ import { describe, it } from 'mocha' -import { settle, resolve, reject } from '../src/main' -import { isFulfilled, isRejected } from '../src/inspect' +import { settle, resolve, reject, isFulfilled, isRejected } from '../src/main' import { throwingIterable } from './lib/test-util' import assert from 'assert' diff --git a/test/timeout-test.js b/test/timeout-test.js index fa9f063..29e8339 100644 --- a/test/timeout-test.js +++ b/test/timeout-test.js @@ -1,8 +1,7 @@ import { describe, it } from 'mocha' -import { timeout, delay } from '../src/main' +import { reject, fulfill, timeout, delay } from '../src/main' import TimeoutError from '../src/TimeoutError' -import { Future, reject, fulfill } from '../src/Promise' -import { silenceError } from '../src/inspect' +import { Future, silenceError } from '../src/Promise' import assert from 'assert' function delayReject (ms, e) { diff --git a/test/toString-test.js b/test/toString-test.js index ef9a282..6bcd009 100644 --- a/test/toString-test.js +++ b/test/toString-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { fulfill, reject, Future, never } from '../src/Promise' -import { getValue, getReason } from '../src/inspect' +import { fulfill, reject, never, getValue, getReason } from '../src/main' +import { Future } from '../src/Promise' import assert from 'assert' describe('toString', () => { From 054ef5c44ab517eb5e4ce2b9686772e7664b1bab Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 21 Jun 2016 09:18:56 +0200 Subject: [PATCH 06/13] alternative approach to shorten reference chains Store `.ref`erences on the `Action`s (now generalised as `Handle`s) instead of directly on the `Future`s. That way, when a handle is passed down `near()` to the eventual resolution, even the reference from the outermost future is only two hops (`.handle.ref`) instead of many. --- src/Action.js | 10 +++++- src/Handle.js | 24 +++++++++++++ src/Promise.js | 81 ++++++++++++++++++++++-------------------- src/coroutine.js | 7 ++-- test/coroutine-test.js | 33 ++++++++++++++++- 5 files changed, 112 insertions(+), 43 deletions(-) create mode 100644 src/Handle.js diff --git a/src/Action.js b/src/Action.js index 72a3f11..79eba6b 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,5 +1,8 @@ -export default class Action { +import Handle from './Handle' + +export default class Action extends Handle { constructor (promise) { + super(null) this.promise = promise } @@ -25,4 +28,9 @@ export default class Action { } // else this.handle(result) } + + run () { + this.ref._runAction(this) + super.run() + } } diff --git a/src/Handle.js b/src/Handle.js new file mode 100644 index 0000000..da95ec7 --- /dev/null +++ b/src/Handle.js @@ -0,0 +1,24 @@ +export default class Handle { + constructor (ref) { + this.ref = ref + this.length = 0 + } + near () { + if (this.ref.handle !== this) { + this.ref = this.ref.near() + } + return this.ref + } + _add (action) { + this[this.length++] = action + // potential for flattening the tree here + return this + } + run () { + for (let i = 0; i < this.length; ++i) { + this.ref._runAction(this[i]) + this[i] = void 0 + } + this.length = 0 + } +} diff --git a/src/Promise.js b/src/Promise.js index 25ca95f..4434921 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -6,6 +6,8 @@ import { TaskQueue, Continuation } from './TaskQueue' import ErrorHandler from './ErrorHandler' import emitError from './emitError' +import Handle from './Handle' + import Action from './Action' import then from './then' import map from './map' @@ -48,9 +50,7 @@ class Core { export class Future extends Core { constructor () { super() - this.ref = void 0 - this.action = void 0 - this.length = 0 + this.handle = void 0 } // then :: Promise e a -> (a -> b) -> Promise e b @@ -110,21 +110,21 @@ export class Future extends Core { // near :: Promise e a -> Promise e a near () { - if (!this._isResolved()) { + if (this.handle === void 0) { return this } - - this.ref = this.ref.near() - return this.ref + return this.handle.near() } // state :: Promise e a -> Int state () { - return this._isResolved() ? this.ref.near().state() : PENDING + // return this._isResolved() ? this.handle.near().state() : PENDING + var n = this.near() + return n === this ? PENDING : n.state() } _isResolved () { - return this.ref !== void 0 + return this.near() !== this } _when (action) { @@ -132,55 +132,60 @@ export class Future extends Core { } _runAction (action) { - if (this.action === void 0) { - this.action = action + if (this.handle) { + this.handle._add(action) + } else if (action.ref != null && action.ref !== this) { + this.handle = new Handle(this) + this.handle._add(action) } else { - this[this.length++] = action + this.handle = action + this.handle.ref = this } } _resolve (x) { - this._become(resolve(x)) - } - - _fulfill (x) { - this._become(new Fulfilled(x)) - } - - _reject (e) { + x = resolve(x) if (this._isResolved()) { return } - - this.__become(new Rejected(e)) + this._become(x) } - _become (p) { + _fulfill (x) { if (this._isResolved()) { return } - - this.__become(p) + this._become(new Fulfilled(x)) } - __become (p) { - this.ref = p === this ? cycle() : p - - if (this.action === void 0) { + _reject (e) { + if (this._isResolved()) { return } - - taskQueue.add(this) + this._become(new Rejected(e)) } - run () { - const p = this.ref.near() - p._runAction(this.action) - this.action = void 0 + _become (p) { + if (p === this) { + p = cycle() + } - for (let i = 0; i < this.length; ++i) { - p._runAction(this[i]) - this[i] = void 0 + if (this.handle) { + // assert: this.handle.ref === this + this.handle.ref = p + if (isSettled(p)) { + taskQueue.add(this.handle) + } else { + p._runAction(this.handle) + } + } else { + if (isSettled(p)) { + // for not unnecessarily creating handles that never see any actions + // works well because it has a near() method + this.handle = p + } else { + this.handle = new Handle(p) + } } } } diff --git a/src/coroutine.js b/src/coroutine.js index e9b9be2..0ec913b 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -2,8 +2,9 @@ import { resolve } from './Promise' import Action from './Action' export default function coroutine (iterator, promise) { - new Coroutine(iterator, promise).run() - // taskQueue.add(new Coroutine(iterator, promise)) + new Coroutine(iterator, promise).start() + // taskQueue.add(new Coroutine(iterator, promise)) // with start for run + // resolve(undefined)._when(new Coroutine(iterator, promise)) return promise } @@ -14,7 +15,7 @@ class Coroutine extends Action { this.throw = iterator.throw.bind(iterator) } - run () { + start () { this.tryCall(this.next, void 0) } diff --git a/test/coroutine-test.js b/test/coroutine-test.js index bae18d5..16e8201 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -34,13 +34,44 @@ describe('coroutine', function () { .then(x => assert.strictEqual(x, expected)) }) + it('should fulfill on return', () => { + const expected = {} + const f = coroutine(function *(a) { + return a + }) + + return f(expected) + .then(x => assert.strictEqual(x, expected)) + }) + it('should reject on uncaught exception', () => { const expected = new Error() const f = coroutine(function *(a) { - yield reject(a) + throw a }) return f(expected) .then(assert.ifError, e => assert.strictEqual(e, expected)) }) + + it('should be able to wait for the same promise multiple times', () => { + const f = coroutine(function *(a) { + var p = fulfill(a) + return (yield p) + (yield p) + }) + + return f('a').then(x => assert.equal(x, 'aa')) + }) + + it('should be able to loop multiple promises', () => { + const f = coroutine(function *(a) { + var arr = [] + for (let i=0; i<5; i++) { + arr.push(yield delay(1, i)) + } + return arr + }) + + return f().then(x => assert.deepEqual(x, [0, 1, 2, 3, 4])) + }) }) From d966354a91da9d906138d481d49015f57966f68b Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 21 Jun 2016 18:01:58 +0200 Subject: [PATCH 07/13] add garbage-counting tests for long/deep chains Inspecting the object reference graph with a simple DFS through properties is possible as Creed does not use any closures for its internals The tests are currently failing :-) --- test/garbage-test.js | 92 ++++++++++++++++++++++++++++ test/lib/refcount-util.js | 125 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 test/garbage-test.js create mode 100644 test/lib/refcount-util.js diff --git a/test/garbage-test.js b/test/garbage-test.js new file mode 100644 index 0000000..5f4fd99 --- /dev/null +++ b/test/garbage-test.js @@ -0,0 +1,92 @@ +import { describe, it } from 'mocha' +import assert from 'assert' + +import { resolve, reject, fulfill, never } from '../src/main' +import getCounts, { collect, formatResults, sumResults } from './lib/refcount-util' + +describe('reference counts', () => { + const C = 3 + const len = C*C*C + it('should be constant for recursive resolving with no handler', () => { + return getCounts(delay => { + // return delay(1).chain( ()=>delay(2).chain( ()=>delay(3) ) ) + function recurse(i) { + return i < len ? delay(i+1).chain(recurse) : fulfill() + } + return recurse(1) + }, (na, ev, co) => sumResults(co)).then(logs => { for (let sum of logs) assert(sum <= C+5, sum+"<="+(C+5)) }) + }) + it('should be constant for recursive resolving with a result', () => { + return getCounts(delay => { + // return delay(1).chain( ()=>delay(2).chain( ()=>delay(3) ) ).map(l => 3-l) + function recurse(i) { + return i < len ? delay(i+1).chain(recurse) : fulfill(i) + } + return recurse(1).map(l => len - l) + }, (na, ev, co) => sumResults(co)).then(logs => { for (let sum of logs) assert(sum <= C+6, sum+"<="+(C+6)) }) + }) + it('should be constant for recursive resolving with a late handler', () => { + return getCounts((delay, timeout) => { + // p = delay(1).chain( ()=>delay(2).chain( ()=>delay(3) ) ) + // largeDelay().then(() => p.map(l => 3-l)) + function recurse(i) { + if (i < len) return delay(i+1).chain(recurse) + timeout(i+1, () => p.map(l => len - l)) + return fulfill(i) + } + var p = recurse(1) + return p + }, (na, ev, co) => sumResults(co)).then(logs => { for (let sum of logs) assert(sum <= C+5, sum+"<="+(C+5)) }) + }) + it('should be not more than linear for recursive resolving with many handlers', () => { + return getCounts(delay => { + function recurse(i) { + if (i>1) p.map(l => len - l) + return i < len ? delay(i+1).chain(recurse) : fulfill(i) + } + var p = recurse(1) + return p + }, (na, ev, co) => [na, co.Futures, co.Maps, sumResults(co)]).then(logs => { + for (let [i, f, m, sum] of logs) { + assert(f <= C+i, f+"<="+(C+i)+" Futures") // 1 Future result per handler + assert(m <= C+i, m+"<="+(C+i)+" Maps") // 1 Map action per handler + sum -= f + m + assert(sum <= C+5, sum+"<="+(C+5)+" others") + } + }) + }) + it('should be constant for recursive resolving with many late handlers', () => { + return getCounts((delay, timeout) => { + // p = delay(1).chain( ()=>delay(2).chain( ()=>delay(3) ) ) + // largeDelay().then(() => { p.map(l => 3-l); p.map(l => 3-l); p.map(l => 3-l); }) + function recurse(i) { + if (i < len) return delay(i+1).chain(recurse) + timeout(i+1, () => { for (var j=0; j len - l) }) // dropping the results + return fulfill(i) + } + var p = recurse(1) + return p + }, (na, ev, co) => sumResults(co)).then(logs => { for (let sum of logs) assert(sum <= C+5, sum+"<="+(C+5)) }) + }) + + /* + function log (na, ev, co) { + console.log("\t"+na+" - "+ev+" ["+formatResults(co)+"]") + } + it('should be interesting1', () => { + return getCounts(delay => { + return delay(1).chain(()=>delay(2)).chain(()=>delay(3)) + }, log) + }) + it('should be interesting2', () => { + return getCounts(delay => { + return delay(1).chain(()=>delay(2).chain(()=>delay(3).chain(()=>delay(4).chain(()=>delay(5))))) + }, log) + }) + it('should be interesting3', () => { + return getCounts(delay => { + return delay(delay(delay(0, 1), 2), 3) + }, log) + }) + */ +}) diff --git a/test/lib/refcount-util.js b/test/lib/refcount-util.js new file mode 100644 index 0000000..aa51341 --- /dev/null +++ b/test/lib/refcount-util.js @@ -0,0 +1,125 @@ +import { resolve, isRejected, isNever } from '../../src/main' +import { Future } from '../../src/Promise' +import Action from '../../src/Action' + +const knownNames = ['Handle', 'Action', + 'Then', 'Chain', 'Map', 'Delay', + 'Future', 'Fulfilled', 'Rejected', + 'Settle', 'Merge', 'Any', 'Race'] +const DELAY_TIME = 5 + +export default function getCounts(exec, report, targets = knownNames, withResult = false) { + function asyncReport(x) { + timeoutCount++ + setTimeout(() => { + res.push(report(x, 'result after', collect([result], targets, seenBefore))) + if (globals.size > +withResult) res.push(report(x, 'globals after', collect(globals, targets, seenBefore))) + if (--timeoutCount == 0) promise._fulfill(res) + }, 2) // after the TaskQueue is drained + } + function monitoredTimeout(log, fn, captures = []) { + for (let c of captures) + globals.add(c) + timeoutCount++ + setTimeout(() => { + for (let c of captures) + globals.delete(c) + if (captures.length) res.push(report(log, 'captures from timeout', collect(captures, targets, seenBefore))) + res.push(report(log, 'result before', collect([result], targets, seenBefore))) + if (globals.size > +withResult) res.push(report(log, 'globals before', collect(globals, targets, seenBefore))) + fn() + asyncReport(log) + if (--timeoutCount == 0) promise._fulfill(res) + }, DELAY_TIME) + } + function monitoredDelay (x, id) { + const p = resolve(x) + if (isRejected(p) || isNever(p)) { + return p + } else { + var promise = new Future() + p._runAction(new Delay(promise, id)) + return promise + } + } + class Delay extends Action { + constructor (promise, id) { + super(promise) + this.id = id + } + + fulfilled (p) { + monitoredTimeout(this.id || p.value, () => this.promise._become(p), [this.promise, p]) + } + } + + const promise = new Future + var timeoutCount = 0 + const res = [] + const seenBefore = new WeakSet + const globals = new Set + const result = exec(monitoredDelay, monitoredTimeout); + if (withResult) { + globals.add(result) + } + asyncReport(0) // 'init' + return promise +} + +export function collect(sources, targets = knownNames, seenBefore = new WeakSet) { + var counts = {} + for (let name of targets) { + counts[name+'s'] = 0 + counts['new'+name+'s'] = 0 + } + var seen = new WeakSet([]) + function test(o) { + if (seen.has(o)) { + return + } else { + seen.add(o) + } + + const name = o.constructor.name + if (targets.includes(name)) { + counts[name+'s']++ + if (!seenBefore.has(o)) { + counts['new'+name+'s']++ + } + } else { + console.log('unknown kind:', o) + } + seenBefore.add(o) + + for (let property in o) { + let value = o[property] + if (value && typeof value == 'object') { // ignore function objects + test(value) + } + } + } + for (let source of sources) { + test(source) + } + return counts +} +export function formatResults(counts) { + var x = [] + for (let name in counts) { + if (counts[name] == 0) continue + if (/^new/.test(name)) continue + let r = name+': '+counts[name] + if (counts['new'+name] > 0) + r += ' (+'+counts['new'+name]+')' + x.push(r) + } + return x.join(', ') +} + +export function sumResults(counts) { + var c = 0; + for (let name in counts) + if (!/^new/.test(name)) + c += counts[name] + return c +} \ No newline at end of file From 0221b91681e93692ee893c3eda6e50fbf29b2ce3 Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 21 Jun 2016 18:38:41 +0200 Subject: [PATCH 08/13] improve garbage collection by shortening `.handle` reference chains Extending test suite to get coverage for such cases --- src/Promise.js | 6 +++++- test/future-test.js | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Promise.js b/src/Promise.js index 4434921..0ddb598 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -166,6 +166,7 @@ export class Future extends Core { } _become (p) { + /* eslint complexity:[2,6] */ if (p === this) { p = cycle() } @@ -183,8 +184,11 @@ export class Future extends Core { // for not unnecessarily creating handles that never see any actions // works well because it has a near() method this.handle = p + } else if (p.handle) { + this.handle = p.handle } else { - this.handle = new Handle(p) + // explicit handle to avoid reference chain between multiple futures + this.handle = p.handle = new Handle(p) } } } diff --git a/test/future-test.js b/test/future-test.js index 158b2fd..99249b1 100644 --- a/test/future-test.js +++ b/test/future-test.js @@ -46,6 +46,29 @@ describe('future', () => { resolve(reject(expected)) return promise.then(assert.ifError, x => assert.strictEqual(expected, x)) }) + + it('should resolve with pending promise', () => { + const { resolve: resolve1, promise: promise1 } = future() + const { resolve: resolve2, promise: promise2 } = future() + const { resolve: resolve3, promise: promise3 } = future() + const expected = {} + setTimeout(resolve2, 1, promise1) + setTimeout(resolve1, 2, expected) + setTimeout(resolve3, 2, promise2) + return promise3.then(x => assert.strictEqual(expected, x)) + }) + + it('should resolve with pending promise that already has handle', () => { + const { resolve: resolve1, promise: promise1 } = future() + const { resolve: resolve2, promise: promise2 } = future() + const { resolve: resolve3, promise: promise3 } = future() + const expected = {} + promise1.map(x => x === expected) + setTimeout(resolve2, 1, promise1) + setTimeout(resolve1, 2, expected) + setTimeout(resolve3, 2, promise2) + return promise3.then(x => assert.strictEqual(expected, x)) + }) }) describe('when resolved to another promise', () => { From 0bf73fbc88fc604d5ad40559d1c4a5c985a9ff50 Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 24 Jun 2016 11:57:07 +0200 Subject: [PATCH 09/13] simplify object counting in collect() --- test/lib/refcount-util.js | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/test/lib/refcount-util.js b/test/lib/refcount-util.js index aa51341..e5681fa 100644 --- a/test/lib/refcount-util.js +++ b/test/lib/refcount-util.js @@ -67,40 +67,39 @@ export default function getCounts(exec, report, targets = knownNames, withResult } export function collect(sources, targets = knownNames, seenBefore = new WeakSet) { + var seenHere = new Set([]) + function test(o) { + if (seenHere.has(o)) return + seenHere.add(o) + + for (let property in o) { + let value = o[property] + if (value && typeof value == 'object') { // ignore function objects + test(value) + } + } + } + for (let source of sources) { + test(source) + } + var counts = {} for (let name of targets) { counts[name+'s'] = 0 counts['new'+name+'s'] = 0 } - var seen = new WeakSet([]) - function test(o) { - if (seen.has(o)) { - return - } else { - seen.add(o) - } - + for (let o of seenHere) { const name = o.constructor.name if (targets.includes(name)) { counts[name+'s']++ if (!seenBefore.has(o)) { + seenBefore.add(o) counts['new'+name+'s']++ } } else { - console.log('unknown kind:', o) - } - seenBefore.add(o) - - for (let property in o) { - let value = o[property] - if (value && typeof value == 'object') { // ignore function objects - test(value) - } + console.log('unknown kind "'+name+'":', o) } } - for (let source of sources) { - test(source) - } return counts } export function formatResults(counts) { From 31163ed53e4da2f7316749ed1a18e32c7459bb2a Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 24 Jun 2016 12:20:30 +0200 Subject: [PATCH 10/13] fix bug in Race.js that I introduced with 0100f8fe but did not notice --- src/Race.js | 2 ++ test/race-test.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Race.js b/src/Race.js index c9610d6..dfa44b5 100644 --- a/src/Race.js +++ b/src/Race.js @@ -6,10 +6,12 @@ export default class Race { } fulfillAt (p, i, promise) { + if (promise._isResolved()) return promise._become(p) } rejectAt (p, i, promise) { + if (promise._isResolved()) return promise._become(p) } diff --git a/test/race-test.js b/test/race-test.js index 4448e04..95defb3 100644 --- a/test/race-test.js +++ b/test/race-test.js @@ -41,4 +41,24 @@ describe('race', () => { return race([reject(1), never()]) .then(assert.ifError, x => assert.equal(x, 1)) }) + + it('should fulfill with first value', () => { + return race([resolve(2).then(resolve), resolve(1)]) + .then(x => assert.equal(x, 1)) + }) + + it('should reject with first value', () => { + return race([resolve(2).then(resolve), reject(1)]) + .then(assert.ifError, x => assert.equal(x, 1)) + }) + + it('should fulfill with first value despite rejection', () => { + return race([resolve(2).then(reject), resolve(1)]) + .then(x => assert.equal(x, 1)) + }) + + it('should reject with first value despite rejection', () => { + return race([resolve(2).then(reject), reject(1)]) + .then(assert.ifError, x => assert.equal(x, 1)) + }) }) From 4b6c9573962d6599ea27e2c7c19138c85c86a6d3 Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 24 Jun 2016 05:00:52 +0200 Subject: [PATCH 11/13] allow Action reusage to optimise Coroutines. Extra Handles will now only be created when a second handler is introduced on a Future, not only because the handler's ref doesn't fit. Also, "unfitting refs" are no more at all :-) --- src/Action.js | 31 ++++++++++++++++++---- src/Handle.js | 18 ++++++++++--- src/Promise.js | 55 +++++++++++++++++++-------------------- src/TaskQueue.js | 14 +--------- src/coroutine.js | 13 +++++---- test/TaskQueue-test.js | 2 +- test/lib/refcount-util.js | 2 +- 7 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/Action.js b/src/Action.js index 79eba6b..29fd3c0 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,11 +1,26 @@ -import Handle from './Handle' +import { Handle, ShareHandle } from './Handle' export default class Action extends Handle { constructor (promise) { - super(null) + super(null) // ref will be set when used as handle this.promise = promise } + _concat (action) { + if (!action._isReused() && (this._isReused() || action instanceof ShareHandle)) { + return action._concat(this) + } else { + return new ShareHandle(this.ref)._concat(this)._concat(action) + } + } + run () { + const settled = this.ref + if (this._isReused()) { + this.ref = null // make action reusable elsewhere + } + settled._runAction(this) + } + // default onFulfilled action /* istanbul ignore next */ fulfilled (p) { @@ -29,8 +44,14 @@ export default class Action extends Handle { this.handle(result) } - run () { - this.ref._runAction(this) - super.run() + tryCallContext (f, c, x) { + let result + try { + result = f.call(c, x) + } catch (e) { + this.promise._reject(e) + return + } // else + this.handle(result) } } diff --git a/src/Handle.js b/src/Handle.js index da95ec7..b0bc46d 100644 --- a/src/Handle.js +++ b/src/Handle.js @@ -1,7 +1,6 @@ -export default class Handle { +export class Handle { constructor (ref) { this.ref = ref - this.length = 0 } near () { if (this.ref.handle !== this) { @@ -9,7 +8,20 @@ export default class Handle { } return this.ref } - _add (action) { + // the ref will be lost, e.g. when an action is used multiple times + _isReused () { + return false // ref is stable by default + } +} + +export class ShareHandle extends Handle { + constructor (ref) { + // assert: ref != null + super(ref) + this.length = 0 + } + _concat (action) { + action.ref = this // a ShareHandle is not a Promise with a .handle, but .near() is enough this[this.length++] = action // potential for flattening the tree here return this diff --git a/src/Promise.js b/src/Promise.js index 0ddb598..d21ebab 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -1,13 +1,12 @@ import { isObject } from './util' import { PENDING, FULFILLED, REJECTED, NEVER, HANDLED } from './state' import { isNever, isSettled } from './inspect' +import { ShareHandle } from './Handle' -import { TaskQueue, Continuation } from './TaskQueue' +import TaskQueue from './TaskQueue' import ErrorHandler from './ErrorHandler' import emitError from './emitError' -import Handle from './Handle' - import Action from './Action' import then from './then' import map from './map' @@ -50,7 +49,7 @@ class Core { export class Future extends Core { constructor () { super() - this.handle = void 0 + this.handle = void 0 // becomes something with a near() method } // then :: Promise e a -> (a -> b) -> Promise e b @@ -132,14 +131,13 @@ export class Future extends Core { } _runAction (action) { + // assert: this.handle is not a Settled promise if (this.handle) { - this.handle._add(action) - } else if (action.ref != null && action.ref !== this) { - this.handle = new Handle(this) - this.handle._add(action) + this.handle = this.handle._concat(action) } else { + // assert: action.ref == null || action.ref.handle == action + action.ref = this this.handle = action - this.handle.ref = this } } @@ -166,30 +164,27 @@ export class Future extends Core { } _become (p) { - /* eslint complexity:[2,6] */ + /* eslint complexity:[2,8] */ if (p === this) { p = cycle() } - - if (this.handle) { - // assert: this.handle.ref === this - this.handle.ref = p - if (isSettled(p)) { + if (isSettled(p) || isNever(p)) { + if (this.handle) { + // assert: this.handle.ref === this + this.handle.ref = p taskQueue.add(this.handle) - } else { - p._runAction(this.handle) } + this.handle = p // works well because it has a near() method } else { - if (isSettled(p)) { - // for not unnecessarily creating handles that never see any actions - // works well because it has a near() method - this.handle = p - } else if (p.handle) { - this.handle = p.handle - } else { - // explicit handle to avoid reference chain between multiple futures - this.handle = p.handle = new Handle(p) + if (this.handle) { + // assert: this.handle.ref === this + p._runAction(this.handle) + } else if (!p.handle) { + p.handle = new ShareHandle(p) + } else if (p.handle._isReused()) { + p.handle = new ShareHandle(p)._concat(p.handle) } + this.handle = p.handle // share handle to avoid reference chain between multiple futures } } } @@ -243,7 +238,9 @@ class Fulfilled extends Core { } _when (action) { - taskQueue.add(new Continuation(action, this)) + // assert: action.ref == null || action.ref === this + action.ref = this + taskQueue.add(action) } _runAction (action) { @@ -302,7 +299,9 @@ class Rejected extends Core { } _when (action) { - taskQueue.add(new Continuation(action, this)) + // assert: action.ref == null || action.ref === this + action.ref = this + taskQueue.add(action) } _runAction (action) { diff --git a/src/TaskQueue.js b/src/TaskQueue.js index d880b7a..14dd40e 100644 --- a/src/TaskQueue.js +++ b/src/TaskQueue.js @@ -1,6 +1,6 @@ import makeAsync from './async' -export class TaskQueue { +export default class TaskQueue { constructor () { this.tasks = new Array(2 << 15) this.length = 0 @@ -24,15 +24,3 @@ export class TaskQueue { this.length = 0 } } - -// make an Action runnable on a Promise -export class Continuation { - constructor (action, promise) { - this.action = action - this.promise = promise - } - - run () { - this.promise._runAction(this.action) - } -} diff --git a/src/coroutine.js b/src/coroutine.js index 0ec913b..4711dd3 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -11,12 +11,15 @@ export default function coroutine (iterator, promise) { class Coroutine extends Action { constructor (iterator, promise) { super(promise) - this.next = iterator.next.bind(iterator) - this.throw = iterator.throw.bind(iterator) + this.iterator = iterator + } + + _isReused () { + return true } start () { - this.tryCall(this.next, void 0) + this.tryCallContext(this.iterator.next, this.iterator, void 0) } handle (result) { @@ -28,11 +31,11 @@ class Coroutine extends Action { } fulfilled (ref) { - this.tryCall(this.next, ref.value) + this.tryCallContext(this.iterator.next, this.iterator, ref.value) } rejected (ref) { - this.tryCall(this.throw, ref.value) + this.tryCallContext(this.iterator.throw, this.iterator, ref.value) return true } } diff --git a/test/TaskQueue-test.js b/test/TaskQueue-test.js index c39f404..097fba1 100644 --- a/test/TaskQueue-test.js +++ b/test/TaskQueue-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { TaskQueue } from '../src/TaskQueue' +import TaskQueue from '../src/TaskQueue' import assert from 'assert' describe('TaskQueue', () => { diff --git a/test/lib/refcount-util.js b/test/lib/refcount-util.js index e5681fa..b6ff99b 100644 --- a/test/lib/refcount-util.js +++ b/test/lib/refcount-util.js @@ -2,7 +2,7 @@ import { resolve, isRejected, isNever } from '../../src/main' import { Future } from '../../src/Promise' import Action from '../../src/Action' -const knownNames = ['Handle', 'Action', +const knownNames = ['Handle', 'ShareHandle', 'Action', 'Then', 'Chain', 'Map', 'Delay', 'Future', 'Fulfilled', 'Rejected', 'Settle', 'Merge', 'Any', 'Race'] From 395875543f6ff109758eb81942a72ba64e490e51 Mon Sep 17 00:00:00 2001 From: Bergi Date: Sat, 25 Jun 2016 16:52:09 +0200 Subject: [PATCH 12/13] simplify getting reference from handle avoiding checks for possibly not existing `.handle` property introducing a new `near`-like method --- src/Handle.js | 11 ++++------- src/Promise.js | 13 +++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Handle.js b/src/Handle.js index b0bc46d..9bbe22a 100644 --- a/src/Handle.js +++ b/src/Handle.js @@ -1,12 +1,9 @@ export class Handle { constructor (ref) { - this.ref = ref + this.ref = ref // a ShareHandle, known Promise or unresolved Future } - near () { - if (this.ref.handle !== this) { - this.ref = this.ref.near() - } - return this.ref + _getRef () { + return this.ref._getRef() } // the ref will be lost, e.g. when an action is used multiple times _isReused () { @@ -21,7 +18,7 @@ export class ShareHandle extends Handle { this.length = 0 } _concat (action) { - action.ref = this // a ShareHandle is not a Promise with a .handle, but .near() is enough + action.ref = this this[this.length++] = action // potential for flattening the tree here return this diff --git a/src/Promise.js b/src/Promise.js index d21ebab..cb12eed 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -36,6 +36,11 @@ class Core { static of (x) { return fulfill(x) } + + _getRef () { + // assert: isNever(this) || !isPending(this) + return this + } } // data Promise e a where @@ -49,7 +54,7 @@ class Core { export class Future extends Core { constructor () { super() - this.handle = void 0 // becomes something with a near() method + this.handle = void 0 // becomes something with a _getRef() method } // then :: Promise e a -> (a -> b) -> Promise e b @@ -112,12 +117,11 @@ export class Future extends Core { if (this.handle === void 0) { return this } - return this.handle.near() + return this.handle._getRef() } // state :: Promise e a -> Int state () { - // return this._isResolved() ? this.handle.near().state() : PENDING var n = this.near() return n === this ? PENDING : n.state() } @@ -127,6 +131,7 @@ export class Future extends Core { } _when (action) { + // assert: !this._isResolved() this._runAction(action) } @@ -174,7 +179,7 @@ export class Future extends Core { this.handle.ref = p taskQueue.add(this.handle) } - this.handle = p // works well because it has a near() method + this.handle = p // works well because it has a _getRef() method } else { if (this.handle) { // assert: this.handle.ref === this From a93506fdd3fe3e7fc82f9f7a5a9f61d9f9581095 Mon Sep 17 00:00:00 2001 From: Bergi Date: Sun, 3 Jul 2016 14:56:52 +0200 Subject: [PATCH 13/13] nearing handles as soon as they are no more necessary hopefully a perf/memory improvement :-) --- src/Handle.js | 2 +- src/Promise.js | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Handle.js b/src/Handle.js index 9bbe22a..48e727f 100644 --- a/src/Handle.js +++ b/src/Handle.js @@ -3,7 +3,7 @@ export class Handle { this.ref = ref // a ShareHandle, known Promise or unresolved Future } _getRef () { - return this.ref._getRef() + return this.ref } // the ref will be lost, e.g. when an action is used multiple times _isReused () { diff --git a/src/Promise.js b/src/Promise.js index cb12eed..892af9d 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -41,6 +41,11 @@ class Core { // assert: isNever(this) || !isPending(this) return this } + + _getHandle () { + // assert: this is a Fulfilled, Rejected or Never + return this + } } // data Promise e a where @@ -114,10 +119,19 @@ export class Future extends Core { // near :: Promise e a -> Promise e a near () { - if (this.handle === void 0) { + let h = this.handle + if (h === void 0) { return this } - return this.handle._getRef() + let ref = h._getRef() + if (ref !== this && ref !== h) { + do { + h = ref + ref = h._getRef() + } while (ref !== h) + this.handle = ref._getHandle() + } + return ref } // state :: Promise e a -> Int @@ -126,6 +140,10 @@ export class Future extends Core { return n === this ? PENDING : n.state() } + _getHandle () { + return this.handle + } + _isResolved () { return this.near() !== this } @@ -170,6 +188,7 @@ export class Future extends Core { _become (p) { /* eslint complexity:[2,8] */ + // assert: p is not a resolved future if (p === this) { p = cycle() }