From 932abc1076a1e2dfff07dc53ca172aa2167a8d1f Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 20 Jun 2016 02:08:19 +0200 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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 f1993b8b15b0b2d164695c864beb04346e78b531 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 22 Jun 2016 01:54:29 +0200 Subject: [PATCH 06/28] first sketch of CancelToken no subscription yet --- src/CancelToken.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/CancelToken.js diff --git a/src/CancelToken.js b/src/CancelToken.js new file mode 100644 index 0000000..d23be5b --- /dev/null +++ b/src/CancelToken.js @@ -0,0 +1,32 @@ +import { resolve } from './Promise' + +export default class CancelToken { + // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor + constructor (executor) { + if (typeof executor !== 'function') { + throw new TypeError('must provide an executor function') + } + this._cancelled = false + executor(reason => this._cancel(reason)) + } + _cancel (reason) { + if (this._cancelled) return + this._cancelled = true + } + // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested + get requested () { + return this._cancelled + } + // https://domenic.github.io/cancelable-promise/#sec-canceltoken.source + static source () { + // optimise case if (this === CancelToken) + let cancel + const token = new this(c => { cancel = c }) + return {token, cancel} + } + static for (thenable) { + return new this(cancel => resolve(thenable).then(cancel)) // finally? + } + static from (cancelTokenlike) { + } +} From 8c93e14beae114fd6a3c774ea16dd7f6fb5e0898 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 22 Jun 2016 04:06:51 +0200 Subject: [PATCH 07/28] add `token` parameters to all functions that need it * implemented token only for Fulfilled, Rejected and Never yet * "need it" is subjective. I've included all obviously cancellable actions, and excluded pure combinators. `merge` should have a token, but it's difficult given its signature * for some reason jsinspect detects a large similarity between Fulfilled and Rejected now, but letting it check for identifiers fixes that (?) --- .jsinspectrc | 1 + src/CancelToken.js | 8 +++- src/Promise.js | 104 +++++++++++++++++++++++++-------------------- src/main.js | 8 ++-- 4 files changed, 71 insertions(+), 50 deletions(-) diff --git a/.jsinspectrc b/.jsinspectrc index 8bfd709..ce70d0a 100644 --- a/.jsinspectrc +++ b/.jsinspectrc @@ -1,3 +1,4 @@ { + "identifiers": true, "threshold": 35 } diff --git a/src/CancelToken.js b/src/CancelToken.js index d23be5b..a7e5fac 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,4 +1,4 @@ -import { resolve } from './Promise' +import { resolve, reject } from './Promise' export default class CancelToken { // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor @@ -13,6 +13,9 @@ export default class CancelToken { if (this._cancelled) return this._cancelled = true } + getRejected () { + return reject(this.reason) + } // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested get requested () { return this._cancelled @@ -28,5 +31,8 @@ export default class CancelToken { return new this(cancel => resolve(thenable).then(cancel)) // finally? } static from (cancelTokenlike) { + if (cancelTokenlike instanceof CancelToken) { + return cancelTokenlike + } } } diff --git a/src/Promise.js b/src/Promise.js index 25ca95f..80d10cc 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -11,6 +11,8 @@ import then from './then' import map from './map' import chain from './chain' +import CancelToken from './CancelToken' + import { race } from './combinators' export const taskQueue = new TaskQueue() @@ -46,44 +48,45 @@ class Core { // Future :: Promise e a // A promise whose value cannot be known until some future time export class Future extends Core { - constructor () { + constructor (token) { super() this.ref = void 0 this.action = void 0 + this.token = token != null ? CancelToken.from(token) : null this.length = 0 } // then :: Promise e a -> (a -> b) -> Promise e b // then :: Promise e a -> () -> (e -> b) -> Promise e b // then :: Promise e a -> (a -> b) -> (e -> b) -> Promise e b - then (f, r) { + then (f, r, token) { const n = this.near() - return n === this ? then(f, r, n, new Future()) : n.then(f, r) + return n === this ? then(f, r, n, new Future(token)) : n.then(f, r, token) } // catch :: Promise e a -> (e -> b) -> Promise e b - catch (r) { + catch (r, token) { const n = this.near() - return n === this ? then(void 0, r, n, new Future()) : n.catch(r) + return n === this ? then(void 0, r, n, new Future(token)) : n.catch(r, token) } // map :: Promise e a -> (a -> b) -> Promise e b - map (f) { + map (f, token) { const n = this.near() - return n === this ? map(f, n, new Future()) : n.map(f) + return n === this ? map(f, n, new Future(token)) : n.map(f, token) } // ap :: Promise e (a -> b) -> Promise e a -> Promise e b - ap (p) { + ap (p, token) { const n = this.near() const pp = p.near() - return n === this ? this.chain(f => pp.map(f)) : n.ap(pp) + return n === this ? this.chain(f => pp.map(f, token)) : n.ap(pp, token) } // chain :: Promise e a -> (a -> Promise e b) -> Promise e b - chain (f) { + chain (f, token) { const n = this.near() - return n === this ? chain(f, n, new Future()) : n.chain(f) + return n === this ? chain(f, n, new Future(token)) : n.chain(f, token) } // concat :: Promise e a -> Promise e a -> Promise e a @@ -193,27 +196,27 @@ class Fulfilled extends Core { this.value = x } - then (f) { - return typeof f === 'function' ? then(f, void 0, this, new Future()) : this + then (f, _, token) { + return typeof f === 'function' ? then(f, void 0, this, new Future(token)) : rejectedIfCancelled(token, this) } - catch () { - return this + catch (_, token) { + return rejectedIfCancelled(token, this) } - map (f) { - return map(f, this, new Future()) + map (f, token) { + return map(f, this, new Future(token)) } - ap (p) { - return p.map(this.value) + ap (p, token) { + return p.map(this.value, token) } - chain (f) { - return chain(f, this, new Future()) + chain (f, token) { + return chain(f, this, new Future(token)) } - concat () { + concat (_) { return this } @@ -252,27 +255,27 @@ class Rejected extends Core { errorHandler.track(this) } - then (_, r) { - return typeof r === 'function' ? this.catch(r) : this + then (_, r, token) { + return typeof r === 'function' ? this.catch(r, token) : rejectedIfCancelled(token, this) } - catch (r) { - return then(void 0, r, this, new Future()) + catch (r, token) { + return then(void 0, r, this, new Future(token)) } - map () { - return this + map (_, token) { + return rejectedIfCancelled(token, this) } - ap () { - return this + ap (_, token) { + return rejectedIfCancelled(token, this) } - chain () { - return this + chain (_, token) { + return rejectedIfCancelled(token, this) } - concat () { + concat (_) { return this } @@ -306,24 +309,24 @@ class Rejected extends Core { // Never :: Promise e a // A promise that waits forever for its value to be known class Never extends Core { - then () { - return this + then (_, __, token) { + return rejectedWhenCancel(token, this) } - catch () { - return this + catch (_, token) { + return rejectedWhenCancel(token, this) } - map () { - return this + map (_, token) { + return rejectedWhenCancel(token, this) } - ap () { - return this + ap (_, token) { + return rejectedWhenCancel(token, this) } - chain () { - return this + chain (_, token) { + return rejectedWhenCancel(token, this) } concat (b) { @@ -396,8 +399,8 @@ export function fulfill (x) { // future :: () -> { resolve: Resolve e a, promise: Promise e a } // type Resolve e a = a|Thenable e a -> () -export function future () { - const promise = new Future() +export function future (token) { + const promise = new Future(token) return {resolve: x => promise._resolve(x), promise} } @@ -410,6 +413,17 @@ function isPromise (x) { return x instanceof Core } +function rejectedIfCancelled (token, settled) { + if (token == null) return settled + token = CancelToken.from(token) + if (token.requested) return token.getRejected() + return settled +} +function rejectedWhenCancel (token, never) { + if (token == null) return never + return CancelToken.from(token).getRejected() +} + function refForMaybeThenable (x) { try { const then = x.then diff --git a/src/main.js b/src/main.js index c7f3ceb..ca8505e 100644 --- a/src/main.js +++ b/src/main.js @@ -90,11 +90,11 @@ function checkFunction (f) { // ------------------------------------------------------------- // delay :: number -> Promise e a -> Promise e a -export function delay (ms, x) { +export function delay (ms, x, token) { /* eslint complexity:[2,4] */ const p = resolve(x) return ms <= 0 || isRejected(p) || isNever(p) ? p - : _delay(ms, p, new Future()) + : _delay(ms, p, new Future(token)) } // timeout :: number -> Promise e a -> Promise (e|TimeoutError) a @@ -113,8 +113,8 @@ const NOARGS = [] // type Reject e = e -> () // Promise :: (Resolve a -> Reject e -> ()) -> Promise e a class CreedPromise extends Future { - constructor (f) { - super() + constructor (f, token) { + super(token) runResolver(_runPromise, f, void 0, NOARGS, this) } } From a076df52d05599d55ffe0d42b95c8ac5614dc97a Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 23 Jun 2016 05:09:45 +0200 Subject: [PATCH 08/28] WIP: subscription on CancelTokens --- src/Action.js | 21 ++++++++++++++++++++- src/CancelToken.js | 39 +++++++++++++++++++++++++++++++++++++-- src/Promise.js | 13 ++++++++++--- src/chain.js | 10 ++++++++++ src/delay.js | 21 +++++++++++++++++---- src/iterable.js | 1 + src/main.js | 17 +++++++++++++++++ src/map.js | 10 ++++++++++ src/then.js | 21 ++++++++++++++++----- 9 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/Action.js b/src/Action.js index 72a3f11..1246fa0 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,17 +1,36 @@ export default class Action { constructor (promise) { - this.promise = promise + this.promise = promise // the Future which this Action tries to resolve + + const token = promise.token + if (token != null) { + token._subscribe(this) + } + } + + destroy () { + this.promise = null + } + + cancel (p) { + if (this.promise._isResolved()) { // promise checks for cancellation itself + this.destroy() + } } // default onFulfilled action /* istanbul ignore next */ fulfilled (p) { + const token = this.promise.token this.promise._become(p) + if (token != null) token._unsubscribe(this) } // default onRejected action rejected (p) { + const token = this.promise.token this.promise._become(p) + if (token != null) token._unsubscribe(this) return false } diff --git a/src/CancelToken.js b/src/CancelToken.js index a7e5fac..a8d9af6 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,4 +1,4 @@ -import { resolve, reject } from './Promise' +import { Future, resolve, reject, silenceError } from './Promise' // deferred export default class CancelToken { // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor @@ -7,14 +7,49 @@ export default class CancelToken { throw new TypeError('must provide an executor function') } this._cancelled = false + this.promise = void 0 + this.length = 0 executor(reason => this._cancel(reason)) } _cancel (reason) { if (this._cancelled) return this._cancelled = true + const p = reject(reason) // tag as intentionally rejected, p._state |= CANCELLED? + silenceError(p) + if (this.promise !== void 0) { + this.promise._resolve(p) + } else { + this.promise = p + } + return this.run() + } + run () { + /* eslint complexity:[2,4] */ + const result = [] + for (let i = 0; i < this.length; ++i) { + try { + if (this[i].promise) { // not already destroyed + result.push(resolve(this[i].cancel(this.promise))) + } + } catch (e) { + result.push(reject(e)) + } + this[i] = void 0 + } + this.length = 0 + return result + } + _subcribe (action) { + this[this.length++] = action + } + _unsubscribe (action) { + action.destroy() // too simple of course } getRejected () { - return reject(this.reason) + if (this.promise === void 0) { + this.promise = new Future() // never cancelled :-) + } + return this.promise } // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested get requested () { diff --git a/src/Promise.js b/src/Promise.js index 80d10cc..62b0073 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -127,7 +127,12 @@ export class Future extends Core { } _isResolved () { - return this.ref !== void 0 + if (this.ref !== void 0) return true + if (this.token != null && this.token.requested) { + this.__become(this.token.getRejected()) + return true + } + return false } _when (action) { @@ -168,6 +173,7 @@ export class Future extends Core { __become (p) { this.ref = p === this ? cycle() : p + this.token = null if (this.action === void 0) { return @@ -178,13 +184,14 @@ export class Future extends Core { run () { const p = this.ref.near() - p._runAction(this.action) + if (this.action.promise) p._runAction(this.action) this.action = void 0 for (let i = 0; i < this.length; ++i) { - p._runAction(this[i]) + if (this[i].promise) p._runAction(this[i]) this[i] = void 0 } + this.length = 0 } } diff --git a/src/chain.js b/src/chain.js index 5ce7bf1..73ed43a 100644 --- a/src/chain.js +++ b/src/chain.js @@ -2,6 +2,9 @@ import { isObject } from './util' import Action from './Action' export default function chain (f, p, promise) { + if (promise.token != null && promise.token.requested) { + return promise.token.getRejected() + } p._when(new Chain(f, promise)) return promise } @@ -12,8 +15,15 @@ class Chain extends Action { this.f = f } + destroy () { + super.destroy() + this.f = null + } + fulfilled (p) { + const token = this.promise.token this.tryCall(this.f, p.value) + if (token != null) token._unsubscribe(this) } handle (y) { diff --git a/src/delay.js b/src/delay.js index aaad5bf..2341898 100644 --- a/src/delay.js +++ b/src/delay.js @@ -9,14 +9,27 @@ class Delay extends Action { constructor (time, promise) { super(promise) this.time = time + this.id = null + } + + destroy () { + super.destroy() + this.time = 0 + if (this.id) { + /* global clearTimeout */ + clearTimeout(this.id) + this.id = null + } } fulfilled (p) { - /*global setTimeout*/ - setTimeout(become, this.time, p, this.promise) + /* global setTimeout */ + this.id = setTimeout(become, this.time, p, this) } } -function become (p, promise) { - promise._become(p) +function become (p, action) { + const token = action.promise.token + action.promise._become(p) + if (token != null) token._unsubscribe(action) } diff --git a/src/iterable.js b/src/iterable.js index 07b4efe..1bcaf60 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -79,6 +79,7 @@ function handleItem (handler, x, i, promise) { class Indexed extends Action { constructor (handler, i, promise) { + // assert: promise.token == null - this is never cancelled super(promise) this.i = i this.handler = handler diff --git a/src/main.js b/src/main.js index ca8505e..4b3f908 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,8 @@ import { isRejected, isSettled, isNever } from './inspect' export { all, race, any, settle, merge } from './combinators' import { all, race } from './combinators' +import Action from './Action' + import _delay from './delay' import _timeout from './timeout' @@ -115,8 +117,23 @@ const NOARGS = [] class CreedPromise extends Future { constructor (f, token) { super(token) + if (this.token != null) { + if (this.token.requested) { + this._resolve(this.token.getRejected()) + return + } + this.cancelAction = new Action(this) + this.token._subscribe(this.cancelAction) + } runResolver(_runPromise, f, void 0, NOARGS, this) } + __become (p) { + if (this.token != null && this.cancelAction != null) { + this.token._unsubscribe(this.cancelAction) // TODO better solution + this.cancelAction = null + } + super.__become(p) + } } CreedPromise.resolve = resolve diff --git a/src/map.js b/src/map.js index 11f29d5..b56a1fe 100644 --- a/src/map.js +++ b/src/map.js @@ -1,6 +1,9 @@ import Action from './Action' export default function map (f, p, promise) { + if (promise.token != null && promise.token.requested) { + return promise.token.getRejected() + } p._when(new Map(f, promise)) return promise } @@ -11,8 +14,15 @@ class Map extends Action { this.f = f } + destroy () { + super.destroy() + this.f = null + } + fulfilled (p) { + const token = this.promise.token this.tryCall(this.f, p.value) + if (token != null) token._unsubscribe(this) } handle (result) { diff --git a/src/then.js b/src/then.js index deaff82..47db529 100644 --- a/src/then.js +++ b/src/then.js @@ -1,6 +1,9 @@ import Action from './Action' export default function then (f, r, p, promise) { + if (promise.token != null && promise.token.requested) { + return promise.token.getRejected() + } p._when(new Then(f, r, promise)) return promise } @@ -12,6 +15,12 @@ class Then extends Action { this.r = r } + destroy () { + super.destroy() + this.f = null + this.r = null + } + fulfilled (p) { this.runThen(this.f, p) } @@ -21,13 +30,15 @@ class Then extends Action { } runThen (f, p) { - if (typeof f !== 'function') { - this.promise._become(p) - return false - } else { + const token = this.promise.token + const hasHandler = typeof f === 'function' + if (hasHandler) { this.tryCall(f, p.value) - return true + } else { + this.promise._become(p) } + if (token != null) token._unsubscribe(this) + return hasHandler } handle (result) { From 52b4d1832ae2e37278462d1fbaef93ad930526f4 Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 23 Jun 2016 08:40:44 +0200 Subject: [PATCH 09/28] more CancelToken sketching --- src/CancelToken.js | 54 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/CancelToken.js b/src/CancelToken.js index a8d9af6..55ec52d 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,4 +1,5 @@ -import { Future, resolve, reject, silenceError } from './Promise' // deferred +import { Future, resolve, reject, silenceError, taskQueue } from './Promise' // deferred +import { isSettled } from './inspect' export default class CancelToken { // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor @@ -40,10 +41,25 @@ export default class CancelToken { return result } _subcribe (action) { + if (this.requested && this.length === 0) { + taskQueue.add(this) // asynchronous? + } this[this.length++] = action } _unsubscribe (action) { - action.destroy() // too simple of course + action.destroy() // TODO too simple of course + } + subscribe (fn, promise) { + promise = resolve(promise) + this._subscribe({ + cancel (p) { + if (!isSettled(promise)) { + return fn(p.value) + } + } + }) + // TODO unsubscribe when promise settles + return this } getRejected () { if (this.promise === void 0) { @@ -70,4 +86,38 @@ export default class CancelToken { return cancelTokenlike } } + static empty () { + return new this(noop) // NeverCancelToken + } + concat (token) { + return new CancelToken(cancel => { + this.subscribe(cancel) + token.subscribe(cancel) + }) + } +} + +class CancelTokenPool { + constructor (tokens) { + this.token = new CancelToken(noop) + this.reasons = [] + this.count = 0 + this.check = r => { + this.reasons.push(r) + if (--this.count === 0) { + this.token._cancel(this.reasons) // forward return value ??? + this.reasons = null + } + } + if (tokens) this.add(...tokens) + // if (this.count === 0 && !this.token.requested) this.token._cancel() ??? + } + add (...tokens) { + if (this.token.requested) return + this.count += tokens.length + for (let t of tokens) CancelToken.from(t).subscribe(this.check) + } + get () { + return this.token + } } From 36dc60d2e84a0edf0bae77edd39c3af60ece5b04 Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 23 Jun 2016 08:45:28 +0200 Subject: [PATCH 10/28] cancel Indexed actions when their promise resolves prematurely --- src/Action.js | 2 +- src/Any.js | 3 +++ src/Merge.js | 4 ++++ src/Promise.js | 5 ++++- src/Race.js | 5 +++++ src/iterable.js | 11 ++++++++--- 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Action.js b/src/Action.js index 1246fa0..cf10171 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,7 +1,7 @@ export default class Action { constructor (promise) { this.promise = promise // the Future which this Action tries to resolve - + // when null, the action is cancelled and won't be executed const token = promise.token if (token != null) { token._subscribe(this) diff --git a/src/Any.js b/src/Any.js index 05f8423..278349a 100644 --- a/src/Any.js +++ b/src/Any.js @@ -1,4 +1,5 @@ import { silenceError } from './Promise' // deferred +import CancelReason from './CancelReason' export default class Any { constructor () { @@ -10,7 +11,9 @@ export default class Any { } fulfillAt (p, i, promise) { + const token = promise.token promise._become(p) + token._cancel(new CancelReason('result is already fulfilled')) } rejectAt (p, i, promise) { diff --git a/src/Merge.js b/src/Merge.js index 4d988e2..6f24377 100644 --- a/src/Merge.js +++ b/src/Merge.js @@ -1,3 +1,5 @@ +import CancelReason from './CancelReason' + export default class Merge { constructor (mergeHandler, results) { this.pending = 0 @@ -15,7 +17,9 @@ export default class Merge { } rejectAt (p, i, promise) { + const token = promise.token promise._become(p) + token._cancel(new CancelReason('result is already rejected', p.value)) } complete (total, promise) { diff --git a/src/Promise.js b/src/Promise.js index 62b0073..a4b4352 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -1,6 +1,6 @@ import { isObject } from './util' import { PENDING, FULFILLED, REJECTED, NEVER, HANDLED } from './state' -import { isNever, isSettled } from './inspect' +import { isRejected, isNever, isSettled } from './inspect' import { TaskQueue, Continuation } from './TaskQueue' import ErrorHandler from './ErrorHandler' @@ -183,12 +183,15 @@ export class Future extends Core { } run () { + /* eslint complexity:[2,6] */ const p = this.ref.near() if (this.action.promise) p._runAction(this.action) + else if (isRejected(p)) silenceError(p) this.action = void 0 for (let i = 0; i < this.length; ++i) { if (this[i].promise) p._runAction(this[i]) + else if (isRejected(p)) silenceError(p) this[i] = void 0 } this.length = 0 diff --git a/src/Race.js b/src/Race.js index c9610d6..846921f 100644 --- a/src/Race.js +++ b/src/Race.js @@ -1,4 +1,5 @@ import { never } from './Promise' // deferred +import CancelReason from './CancelReason' export default class Race { valueAt (x, i, promise) { @@ -6,11 +7,15 @@ export default class Race { } fulfillAt (p, i, promise) { + const token = promise.token promise._become(p) + token._cancel(new CancelReason('result is already fulfilled')) } rejectAt (p, i, promise) { + const token = promise.token promise._become(p) + token._cancel(new CancelReason('result is already rejected', p.value)) } complete (total, promise) { diff --git a/src/iterable.js b/src/iterable.js index 1bcaf60..80012a2 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -1,6 +1,7 @@ -import { isObject } from './util' +import { isObject, noop } from './util' import { Future, reject, resolveObject, silenceError } from './Promise' // deferred import { isFulfilled, isRejected } from './inspect' +import CancelToken from './CancelToken' import Action from './Action' function isIterable (x) { @@ -12,7 +13,7 @@ export function iterablePromise (handler, iterable) { return reject(new TypeError('expected an iterable')) } - const p = new Future() + const p = new Future(new CancelToken(noop)) return resolveIterable(handler, iterable, p) } @@ -79,12 +80,16 @@ function handleItem (handler, x, i, promise) { class Indexed extends Action { constructor (handler, i, promise) { - // assert: promise.token == null - this is never cancelled super(promise) this.i = i this.handler = handler } + _destroy () { + super._destroy() + this.handler = null + } + fulfilled (p) { this.handler.fulfillAt(p, this.i, this.promise) } From 6c47370f213b0610cf0fd354d16afa2cc2b8c0cf Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 23 Jun 2016 09:33:44 +0200 Subject: [PATCH 11/28] some optimisations for CancelToken implementation --- src/CancelToken.js | 48 ++++++++++++++++++++++++++++++++++++++-------- src/util.js | 2 +- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/CancelToken.js b/src/CancelToken.js index 55ec52d..29ce222 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,3 +1,4 @@ +import { noop } from './util' import { Future, resolve, reject, silenceError, taskQueue } from './Promise' // deferred import { isSettled } from './inspect' @@ -10,7 +11,11 @@ export default class CancelToken { this._cancelled = false this.promise = void 0 this.length = 0 - executor(reason => this._cancel(reason)) + this.scanLow = 0 + this.scanHigh = 0 + if (executor !== noop) { + executor(reason => this._cancel(reason)) + } } _cancel (reason) { if (this._cancelled) return @@ -29,7 +34,7 @@ export default class CancelToken { const result = [] for (let i = 0; i < this.length; ++i) { try { - if (this[i].promise) { // not already destroyed + if (this[i] && this[i].promise) { // not already destroyed result.push(resolve(this[i].cancel(this.promise))) } } catch (e) { @@ -47,13 +52,33 @@ export default class CancelToken { this[this.length++] = action } _unsubscribe (action) { - action.destroy() // TODO too simple of course + for (let i=Math.min(5, this.length); i--; ) { + // an inplace-filtering algorithm to remove empty actions + // executed at up to 5 steps per unsubscribe + if (this.scanHigh < this.length) { + if (this[this.scanHigh] === action) { + this[this.scanHigh] = action = null; + } else if (this[this.scanHigh].promise == null) { + this[this.scanHigh] = null; + } else { + this[this.scanLow++] = this[this.scanHigh] + } + this.scanHigh++ + } else { + this.length = this.scanLow; + this.scanLow = this.scanHigh = 0; + } + } + if (action) { // when not found + action._destroy() // at least mark explictly as empty + } } subscribe (fn, promise) { promise = resolve(promise) this._subscribe({ + promise, cancel (p) { - if (!isSettled(promise)) { + if (!isSettled(this.promise)) { return fn(p.value) } } @@ -73,10 +98,17 @@ export default class CancelToken { } // https://domenic.github.io/cancelable-promise/#sec-canceltoken.source static source () { - // optimise case if (this === CancelToken) - let cancel - const token = new this(c => { cancel = c }) - return {token, cancel} + if (this === CancelToken) { + const token = new this(noop) + return { + token, + cancel (r) { token._cancel(r) } + } + } else { + let cancel + const token = new this(c => { cancel = c }) + return {token, cancel} + } } static for (thenable) { return new this(cancel => resolve(thenable).then(cancel)) // finally? diff --git a/src/util.js b/src/util.js index 550c0f7..38d15eb 100644 --- a/src/util.js +++ b/src/util.js @@ -4,4 +4,4 @@ export function isObject (x) { } /* istanbul ignore next */ -export function noop () {} +export function noop (_) {} From c7653bbbecd1871185491cdcfd54af9ddd2311b1 Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 27 Jun 2016 03:17:25 +0200 Subject: [PATCH 12/28] Add tests for CancelToken (with full coverage of CancelToken.js) Also fixing some linting issues, typos and bugs in the cancellation code :-) --- src/CancelReason.js | 2 + src/CancelToken.js | 30 ++- src/iterable.js | 4 +- src/main.js | 2 + test/CancelToken-test.js | 432 +++++++++++++++++++++++++++++++++++++++ test/lib/test-util.js | 39 ++++ 6 files changed, 497 insertions(+), 12 deletions(-) create mode 100644 src/CancelReason.js create mode 100644 test/CancelToken-test.js diff --git a/src/CancelReason.js b/src/CancelReason.js new file mode 100644 index 0000000..f017167 --- /dev/null +++ b/src/CancelReason.js @@ -0,0 +1,2 @@ +export default class CancelReason extends Error { +} diff --git a/src/CancelToken.js b/src/CancelToken.js index 29ce222..088dcb3 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,5 +1,5 @@ import { noop } from './util' -import { Future, resolve, reject, silenceError, taskQueue } from './Promise' // deferred +import { Future, resolve, reject, never, silenceError, taskQueue } from './Promise' // deferred import { isSettled } from './inspect' export default class CancelToken { @@ -45,36 +45,37 @@ export default class CancelToken { this.length = 0 return result } - _subcribe (action) { + _subscribe (action) { if (this.requested && this.length === 0) { taskQueue.add(this) // asynchronous? } this[this.length++] = action } _unsubscribe (action) { - for (let i=Math.min(5, this.length); i--; ) { + /* eslint complexity:[2,6] */ + for (let i = Math.min(5, this.length); i--;) { // an inplace-filtering algorithm to remove empty actions // executed at up to 5 steps per unsubscribe if (this.scanHigh < this.length) { if (this[this.scanHigh] === action) { - this[this.scanHigh] = action = null; + this[this.scanHigh] = action = null } else if (this[this.scanHigh].promise == null) { - this[this.scanHigh] = null; + this[this.scanHigh] = null } else { this[this.scanLow++] = this[this.scanHigh] } this.scanHigh++ } else { - this.length = this.scanLow; - this.scanLow = this.scanHigh = 0; + this.length = this.scanLow + this.scanLow = this.scanHigh = 0 } } if (action) { // when not found - action._destroy() // at least mark explictly as empty + action.destroy() // at least mark explictly as empty } } subscribe (fn, promise) { - promise = resolve(promise) + promise = promise != null ? resolve(promise) : never() this._subscribe({ promise, cancel (p) { @@ -114,6 +115,7 @@ export default class CancelToken { return new this(cancel => resolve(thenable).then(cancel)) // finally? } static from (cancelTokenlike) { + /* istanbul ignore else */ if (cancelTokenlike instanceof CancelToken) { return cancelTokenlike } @@ -122,11 +124,16 @@ export default class CancelToken { return new this(noop) // NeverCancelToken } concat (token) { + if (this.requested) return this + if (token.requested) return token return new CancelToken(cancel => { this.subscribe(cancel) token.subscribe(cancel) }) } + static pool (tokens) { + return new CancelTokenPool(tokens) + } } class CancelTokenPool { @@ -147,7 +154,10 @@ class CancelTokenPool { add (...tokens) { if (this.token.requested) return this.count += tokens.length - for (let t of tokens) CancelToken.from(t).subscribe(this.check) + // for (let t of tokens) { // https://phabricator.babeljs.io/T2164 + for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) { + CancelToken.from(t).subscribe(this.check) + } } get () { return this.token diff --git a/src/iterable.js b/src/iterable.js index 80012a2..1e201f3 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -85,8 +85,8 @@ class Indexed extends Action { this.handler = handler } - _destroy () { - super._destroy() + destroy () { + super.destroy() this.handler = null } diff --git a/src/main.js b/src/main.js index 4b3f908..b36b38f 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,8 @@ import { isRejected, isSettled, isNever } from './inspect' export { all, race, any, settle, merge } from './combinators' import { all, race } from './combinators' +export { default as CancelToken } from './CancelToken' + import Action from './Action' import _delay from './delay' diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js new file mode 100644 index 0000000..ee4b780 --- /dev/null +++ b/test/CancelToken-test.js @@ -0,0 +1,432 @@ +import { describe, it } from 'mocha' +import { CancelToken, isRejected, isPending, getReason, future, reject } from '../src/main' +import { FakeCancelAction, raceCallbacks } from './lib/test-util' +import assert from 'assert' + +describe('CancelToken', function () { + describe('constructor', () => { + it('should synchronously call the executor with a function', () => { + let cancel + new CancelToken(c => { cancel = c }) + assert.strictEqual(typeof cancel, 'function') + }) + + it('should throw synchronously when function not provided', () => { + assert.throws(() => new CancelToken(), TypeError) + }) + + it('should not catch exceptions', () => { + const err = new Error() + assert.throws(() => { + new CancelToken(() => { throw err }) + }, e => e === err) + }) + }) + + describe('static source()', () => { + it('should return a token and a cancel function', () => { + const {token, cancel} = CancelToken.source() + assert(token instanceof CancelToken) + assert.strictEqual(typeof cancel, 'function') + }) + }) + + it('should have a boolean .requested property', () => { + const {token, cancel} = CancelToken.source() + assert.strictEqual(token.requested, false) + cancel() + assert.strictEqual(token.requested, true) + }) + + describe('getRejected()', () => { + it('should return a rejected promise after the token was cancelled', () => { + const {token, cancel} = CancelToken.source() + cancel() + assert(isRejected(token.getRejected())) + }) + + it('should return a pending promise until the token is cancelled', () => { + const {token, cancel} = CancelToken.source() + const p = token.getRejected() + assert(isPending(p)) + cancel() + assert(isRejected(p)) + }) + + it('should reject with the argument of the first cancel call', () => { + const {token, cancel} = CancelToken.source() + const r = {} + cancel(r) + cancel({}) + const p = token.getRejected() + cancel({}) + return p.then(assert.ifError, e => { + assert.strictEqual(e, r) + }) + }) + + it('should return a pending promise until the token is asynchronously cancelled', () => { + const {token, cancel} = CancelToken.source() + const p = token.getRejected() + const r = {} + setTimeout(() => { + assert(isPending(p)) + assert(!token.requested) + cancel(r) + }, 5) + return p.then(assert.ifError, e => { + assert(token.requested) + assert.strictEqual(e, r) + }) + }) + }) + + it('should be subclassible', () => { + let constructorCalled = false + let myCancel + class MyCancelToken extends CancelToken { + constructor (exec) { + constructorCalled = true + super(c => { + assert.strictEqual(typeof c, 'function') + myCancel = () => { c() } + exec(myCancel) + }) + } + } + const {token, cancel} = MyCancelToken.source() + assert(constructorCalled) + assert(token instanceof MyCancelToken) + assert.strictEqual(cancel, myCancel) + cancel() + assert(token.requested) + }) + + describe('_subscribe()', () => { + it('should synchronously run subscriptions', () => { + const {token, cancel} = CancelToken.source() + const r = {} + const action = new FakeCancelAction({}, p => assert.strictEqual(getReason(p), r)) + token._subscribe(action) + assert(!action.isCancelled) + cancel(r) + assert(action.isCancelled) + }) + + it('should not run subscriptions multiple times', () => { + const {token, cancel} = CancelToken.source() + const action = new FakeCancelAction({}) + token._subscribe(action) + assert(!action.isCancelled) + cancel() + cancel() + assert.strictEqual(action.isCancelled, 1) + }) + + it('should not run destroyed subscriptions', () => { + const {token, cancel} = CancelToken.source() + const action = new FakeCancelAction({}) + token._subscribe(action) + action.destroy() + assert(!action.promise) + cancel() + assert(!action.isCancelled) + }) + + it('should not run unsubscribed actions', () => { + const {token, cancel} = CancelToken.source() + const action = new FakeCancelAction({}) + token._subscribe(action) + token._unsubscribe(action) + cancel() + assert(!action.isCancelled) + }) + + it('should do the same with lots of subscriptions', () => { + const {token, cancel} = CancelToken.source() + const active = new Set() + const inactive = new Set() + for (let i = 0; i < 150; i++) { + if (active.size && Math.random() < 0.3) { + const action = Array.from(active)[Math.floor(Math.pow(Math.random(), 1.5) * active.size)] + if (Math.random() < 0.4) { + action.destroy() + } else { + token._unsubscribe(action) + } + active.delete(action) + inactive.add(action) + } else { + const action = new FakeCancelAction({}) + token._subscribe(action) + active.add(action) + } + } + // console.log(active.size, inactive.size) + for (const action of active) assert(!action.isDestroyed && !action.isCancelled) + cancel() + for (const action of active) assert(action.isCancelled) + for (const action of inactive) assert(!action.isCancelled) + }) + + it('should ignore exceptions thrown by subscriptions', () => { + const {token, cancel} = CancelToken.source() + const throwAction = new FakeCancelAction({}, () => { throw new Error() }) + token._subscribe(throwAction) + const action = new FakeCancelAction({}) + token._subscribe(action) + cancel() + assert(action.isCancelled) + }) + + it('should run subscriptions when already requested', () => { + const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() + cancel() + token._subscribe(new FakeCancelAction({}, p => resolve(getReason(p)))) + return promise + }) + }) + + describe('subscribe()', () => { + it('should synchronously call subscriptions', () => { + const {token, cancel} = CancelToken.source() + const r = {} + let isCalled = false + token.subscribe(e => { + isCalled = true + assert.strictEqual(e, r) + }) + assert(!isCalled) + cancel(r) + assert(isCalled) + }) + + it('should not call subscriptions multiple times', () => { + const {token, cancel} = CancelToken.source() + let calls = 0 + token.subscribe(() => { + calls++ + }) + assert.strictEqual(calls, 0) + cancel() + cancel() + assert.strictEqual(calls, 1) + }) + + it('should ignore exceptions thrown by subscriptions', () => { + const {token, cancel} = CancelToken.source() + let isCalled = false + token.subscribe(() => { throw new Error() }) + token.subscribe(e => { + isCalled = true + }) + cancel() + assert(isCalled) + }) + + it('should call subscriptions when already requested', () => { + const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() + cancel() + token.subscribe(resolve) + return promise + }) + + it('should call subscriptions in order', () => { + const {token, cancel} = CancelToken.source() + let s = 0 + token.subscribe(() => { + assert.strictEqual(s, 0) + s = 1 + }) + token.subscribe(() => { + assert.strictEqual(s, 1) + s = 2 + }) + cancel() + assert.strictEqual(s, 2) + }) + + it('should call subscriptions when the promise is not settled', () => { + const {token, cancel} = CancelToken.source() + const {promise} = future() + const {ok, result} = raceCallbacks(future) + token.subscribe(ok, promise) + cancel() + return result + }) + + it('should not call subscriptions when the promise is fulfilled', () => { + const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() + const {ok, nok, result} = raceCallbacks(future) + token.subscribe(nok, promise) + promise.then(cancel).then(ok) + resolve() + return result + }) + + it('should not call subscriptions when the promise is rejected', () => { + const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() + const {ok, nok, result} = raceCallbacks(future) + token.subscribe(nok, promise) + promise.catch(cancel).then(ok) + resolve(reject()) + return result + }) + + it('should return the token', () => { + const {token} = CancelToken.source() + assert.strictEqual(token.subscribe(() => {}), token) + }) + }) + + describe('concat()', () => { + it('should cancel the result token when a is cancelled first', () => { + const a = CancelToken.source() + const b = CancelToken.source() + const token = a.token.concat(b.token) + const r = {} + assert(!token.requested) + a.cancel(r) + assert(token.requested) + b.cancel({}) + return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + }) + + it('should cancel the result token when b is cancelled first', () => { + const a = CancelToken.source() + const b = CancelToken.source() + const token = a.token.concat(b.token) + const r = {} + assert(!token.requested) + b.cancel(r) + assert(token.requested) + a.cancel({}) + return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + }) + + it('should cancel the result token when a is already cancelled', () => { + const a = CancelToken.source() + const b = CancelToken.source() + const r = {} + a.cancel(r) + const token = a.token.concat(b.token) + assert(token.requested) + b.cancel({}) + return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + }) + + it('should cancel the result token when b is already cancelled', () => { + const a = CancelToken.source() + const b = CancelToken.source() + const r = {} + b.cancel(r) + const token = a.token.concat(b.token) + assert(token.requested) + a.cancel({}) + return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + }) + }) + + describe('static empty()', () => { + it('should produce a token that is never cancelled', () => { + const token = CancelToken.empty() + assert(token instanceof CancelToken) + token.subscribe(() => { + setTimeout(assert.ok, 0, false, 'must not be called') + }) + }) + }) + + describe('for()', () => { + it('should cancel the token when the promise fulfills', () => { + const {resolve, promise} = future() + const token = CancelToken.for(promise) + assert(!token.requested) + const r = {} + resolve(r) + return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + }) + }) + + describe('static pool()', () => { + it('should cancel when all tokens are cancelled', () => { + const sources = [] + const reasons = [] + for (let i = 0; i < 5; i++) { + sources.push(CancelToken.source()) + reasons.push({t: i}) + } + const pool = CancelToken.pool(sources.map(s => s.token)) + for (let i = 0; i < sources.length; i++) { + assert(!pool.get().requested) + sources[i].cancel(reasons[i]) + } + return pool.get().getRejected().then(assert.ifError, r => { + assert.deepEqual(r, reasons) + }) + }) + + it('should allow tokens to be added before cancellation', () => { + const pool = CancelToken.pool() + const sources = [] + for (let i = 0; i < 5; i++) { + sources[i] = CancelToken.source() + sources[i].added = true + pool.add(sources[i].token) + } + for (let i = 0; i < 15; i++) { + sources.push(CancelToken.source()) + } + const token = pool.get() + while (sources.some(s => s.added && !s.token.requested)) { + assert(!token.requested) + let s = sources[Math.floor(Math.random() * sources.length)] + if (s.added && s.token.requested) { + s = sources[sources.length - 1] + if (s.added && s.token.requested) { + sources.pop() + continue + } + } + if (!s.added && Math.random() < 0.9) { + if (Math.random() < 0.3) { + const x = CancelToken.source() + sources.push(x) + pool.add(s.token, x.token) + x.added = true + } else { + pool.add(s.token) + } + s.added = true + } else if (!s.token.requested) { + s.cancel() + } + } + return pool.get().getRejected().then(assert.ifError, r => { + assert(token.requested) + }) + }) + + it('should not allow tokens to be added after cancellation', () => { + const pool = CancelToken.pool() + const a = CancelToken.source() + pool.add(a.token) + const b = CancelToken.source() + b.cancel() + pool.add(b.token) + const c = CancelToken.source() + pool.add(c.token) + c.cancel() + a.cancel() + return pool.get().getRejected().then(assert.ifError, () => { + const d = CancelToken.source() + pool.add(d.token) + assert(pool.get().requested) + }) + }) + }) +}) diff --git a/test/lib/test-util.js b/test/lib/test-util.js index d1bd76b..4c49ec1 100644 --- a/test/lib/test-util.js +++ b/test/lib/test-util.js @@ -57,3 +57,42 @@ class ThrowingIterator { throw e } } + +export class FakeCancelAction { + constructor (promise, cb) { + this.promise = promise + this.cb = cb + this.isCancelled = 0 + this.isDestroyed = 0 + const token = promise.token + if (token != null) { + token._subscribe(this) + } + } + + destroy () { + this.isDestroyed++ + this.promise = null + } + + cancel (p) { + this.isCancelled++ + if (typeof this.cb === 'function') this.cb(p) + if (typeof this.promise._isResolved !== 'function' || this.promise._isResolved()) { + this.destroy() + } + } +} + +export function raceCallbacks (future) { + const {resolve, promise} = future() + return { + ok (x) { + setTimeout(resolve, 1, x) // wait for noks + }, + nok (e) { + promise._reject(e) + }, + result: promise + } +} From 94a1f9db061ca05f120f15444b3ee050f6ca7cb6 Mon Sep 17 00:00:00 2001 From: Bergi Date: Mon, 27 Jun 2016 07:58:53 +0200 Subject: [PATCH 13/28] simple token usage tests --- test/fulfill-test.js | 52 ++++++++++++++++++++++++++++++++++--- test/never-test.js | 44 ++++++++++++++++++++++++++----- test/reject-test.js | 62 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 11 deletions(-) diff --git a/test/fulfill-test.js b/test/fulfill-test.js index 65290e4..8a7fc88 100644 --- a/test/fulfill-test.js +++ b/test/fulfill-test.js @@ -1,6 +1,7 @@ import { describe, it } from 'mocha' -import { fulfill, reject, getValue } from '../src/main' +import { fulfill, reject, getValue, CancelToken } from '../src/main' import { silenceError } from '../src/Promise' +import { assertSame } from './lib/test-util' import assert from 'assert' describe('fulfill', () => { @@ -25,13 +26,58 @@ describe('fulfill', () => { return fulfill(x).then(y => assert(x === y)) }) + it('then should be identity without f callback', () => { + const p = fulfill(true) + assert.strictEqual(p, p.then()) + }) + + it('then with uncancelled token should be identity without f callback', () => { + const p = fulfill(true) + assert.strictEqual(p, p.then(null, null, CancelToken.empty())) + }) + + it('then with cancelled token should behave like cancellation', () => { + const p = fulfill(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.then(assert.ifError, assert.ifError, token)) + }) + it('catch should be identity', () => { const p = fulfill(true) assert.strictEqual(p, p.catch(assert.ifError)) }) - it('then should be identity without f callback', () => { + it('catch with uncancelled token should be identity', () => { const p = fulfill(true) - assert.strictEqual(p, p.then()) + assert.strictEqual(p, p.catch(assert.ifError, CancelToken.empty())) + }) + + it('catch with cancelled token should behave like cancellation', () => { + const p = fulfill(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.catch(assert.ifError, token)) + }) + + it('map with cancelled token should behave like cancellation', () => { + const p = fulfill(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.map(assert.ifError, token)) + }) + + it('ap with cancelled token should behave like cancellation', () => { + const p = fulfill(assert.ifError) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.ap(fulfill(true), token)) + }) + + it('chain with cancelled token should behave like cancellation', () => { + const p = fulfill(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.chain(assert.ifError, token)) }) }) diff --git a/test/never-test.js b/test/never-test.js index 084f56a..1db5b0a 100644 --- a/test/never-test.js +++ b/test/never-test.js @@ -1,33 +1,63 @@ import { describe, it } from 'mocha' -import { never, fulfill } from '../src/main' +import { never, fulfill, CancelToken } from '../src/main' import assert from 'assert' describe('never', () => { it('then should be identity', () => { - var p = never() + const p = never() assert.strictEqual(p, p.then(assert.ifError, assert.ifError)) }) + it('then with token should return the cancellation', () => { + const p = never() + const token = CancelToken.empty() + assert.strictEqual(token.getRejected(), p.then(assert.ifError, assert.ifError, token)) + }) + it('catch should be identity', () => { - var p = never() + const p = never() assert.strictEqual(p, p.catch(assert.ifError)) }) + it('catch with token should return the cancellation', () => { + const p = never() + const token = CancelToken.empty() + assert.strictEqual(token.getRejected(), p.catch(assert.ifError, token)) + }) + it('map should be identity', () => { - var p = never() + const p = never() assert.strictEqual(p, p.map(assert.ifError)) }) + it('map with token should return the cancellation', () => { + const p = never() + const token = CancelToken.empty() + assert.strictEqual(token.getRejected(), p.map(assert.ifError, token)) + }) + it('ap should be identity', () => { - var p = never() - assert.strictEqual(p, p.ap(fulfill())) + const p = never() + assert.strictEqual(p, p.ap(fulfill(true))) + }) + + it('ap with token should return the cancellation', () => { + const p = never() + const token = CancelToken.empty() + assert.strictEqual(token.getRejected(), p.ap(fulfill(true), token)) }) it('chain should be identity', () => { - var p = never() + const p = never() assert.strictEqual(p, p.chain(fulfill)) }) + it('chain with token should return the cancellation', () => { + const p = never() + const token = CancelToken.empty() + assert.strictEqual(token.getRejected(), p.chain(assert.ifError, token)) + }) + it('_when should not call action', () => { let fail = () => { throw new Error('never._when called action') } let action = { diff --git a/test/reject-test.js b/test/reject-test.js index 9f0426b..3cdde6b 100644 --- a/test/reject-test.js +++ b/test/reject-test.js @@ -1,6 +1,7 @@ import { describe, it } from 'mocha' -import { reject, fulfill } from '../src/main' +import { reject, fulfill, CancelToken } from '../src/main' import { silenceError } from '../src/Promise' +import { assertSame } from './lib/test-util' import assert from 'assert' describe('reject', () => { @@ -10,21 +11,80 @@ describe('reject', () => { assert.strictEqual(p, p.then(assert.ifError)) }) + it('then with uncancelled token should be identity without r callback', () => { + const p = reject(true) + silenceError(p) + assert.strictEqual(p, p.then(assert.ifError, null, CancelToken.empty())) + }) + + it('then with cancelled token should behave like cancellation', () => { + const p = reject(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.then(assert.ifError, null, token)) + }) + + it('catch with cancelled token should behave like cancellation', () => { + const p = reject(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.catch(assert.ifError, token)) + }) + it('map should be identity', () => { const p = reject(true) silenceError(p) assert.strictEqual(p, p.map(assert.ifError)) }) + it('map with uncancelled token should be identity', () => { + const p = reject(true) + silenceError(p) + assert.strictEqual(p, p.map(assert.ifError, CancelToken.empty())) + }) + + it('map with cancelled token should behave like cancellation', () => { + const p = reject(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.map(assert.ifError, token)) + }) + it('ap should be identity', () => { const p = reject(assert.ifError) silenceError(p) assert.strictEqual(p, p.ap(fulfill(true))) }) + it('ap with uncancelled token should be identity', () => { + const p = reject(assert.ifError) + silenceError(p) + assert.strictEqual(p, p.ap(fulfill(true), CancelToken.empty())) + }) + + it('ap with cancelled token should behave like cancellation', () => { + const p = reject(assert.ifError) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.ap(fulfill(true), token)) + }) + it('chain should be identity', () => { const p = reject() silenceError(p) assert.strictEqual(p, p.chain(fulfill)) }) + + it('chain with uncancelled token should be identity', () => { + const p = reject() + silenceError(p) + assert.strictEqual(p, p.chain(fulfill, CancelToken.empty())) + }) + + it('chain with cancelled token should behave like cancellation', () => { + const p = reject(true) + const {token, cancel} = CancelToken.source() + cancel({}) + return assertSame(token.getRejected(), p.chain(assert.ifError, token)) + }) }) From dcf9db0e57416110ce898d4b3a035dd09600054b Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 28 Jun 2016 22:31:34 +0200 Subject: [PATCH 14/28] Add tests for cancellation * then, catch, map, ap, chain (including edge scenarios) * new Promise * delay (with full coverage of everything!) Also fixing some coverage excludes and bugs in the cancellation code :-) --- src/Action.js | 6 +- src/Promise.js | 4 +- src/TaskQueue.js | 2 +- src/main.js | 8 +- test/Promise-test.js | 80 +- test/cancellation-test.js | 1444 +++++++++++++++++++++++++++++++++++++ test/delay-test.js | 62 +- 7 files changed, 1597 insertions(+), 9 deletions(-) create mode 100644 test/cancellation-test.js diff --git a/src/Action.js b/src/Action.js index cf10171..178f4fb 100644 --- a/src/Action.js +++ b/src/Action.js @@ -13,6 +13,7 @@ export default class Action { } cancel (p) { + /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself this.destroy() } @@ -35,13 +36,14 @@ export default class Action { } tryCall (f, x) { + /* eslint complexity:[2,4] */ let result try { result = f(x) } catch (e) { - this.promise._reject(e) + if (this.promise) this.promise._reject(e) return } // else - this.handle(result) + if (this.promise) this.handle(result) } } diff --git a/src/Promise.js b/src/Promise.js index a4b4352..dc541c0 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -80,7 +80,7 @@ export class Future extends Core { ap (p, token) { const n = this.near() const pp = p.near() - return n === this ? this.chain(f => pp.map(f, token)) : n.ap(pp, token) + return n === this ? this.chain(f => pp.map(f, token), token) : n.ap(pp, token) } // chain :: Promise e a -> (a -> Promise e b) -> Promise e b @@ -251,6 +251,7 @@ class Fulfilled extends Core { } _runAction (action) { + // assert: action.promise != null action.fulfilled(this) } } @@ -310,6 +311,7 @@ class Rejected extends Core { } _runAction (action) { + // assert: action.promise != null if (action.rejected(this)) { errorHandler.untrack(this) } diff --git a/src/TaskQueue.js b/src/TaskQueue.js index d880b7a..6f84e1a 100644 --- a/src/TaskQueue.js +++ b/src/TaskQueue.js @@ -33,6 +33,6 @@ export class Continuation { } run () { - this.promise._runAction(this.action) + if (this.action.promise) this.promise._runAction(this.action) } } diff --git a/src/main.js b/src/main.js index b36b38f..0c788f8 100644 --- a/src/main.js +++ b/src/main.js @@ -95,10 +95,12 @@ function checkFunction (f) { // delay :: number -> Promise e a -> Promise e a export function delay (ms, x, token) { - /* eslint complexity:[2,4] */ + /* eslint complexity:[2,5] */ + if (token != null && token.requested) return token.getRejected() const p = resolve(x) - return ms <= 0 || isRejected(p) || isNever(p) ? p - : _delay(ms, p, new Future(token)) + if (ms <= 0) return p + if (token == null && (isRejected(p) || isNever(p))) return p + return _delay(ms, p, new Future(token)) } // timeout :: number -> Promise e a -> Promise (e|TimeoutError) a diff --git a/test/Promise-test.js b/test/Promise-test.js index e9b288e..5149c00 100644 --- a/test/Promise-test.js +++ b/test/Promise-test.js @@ -1,5 +1,6 @@ import { describe, it } from 'mocha' -import { Promise, fulfill, reject } from '../src/main' +import { Promise, fulfill, reject, isRejected, CancelToken } from '../src/main' +import { assertSame } from './lib/test-util' import assert from 'assert' describe('Promise', () => { @@ -13,6 +14,17 @@ describe('Promise', () => { return p }) + it('should not call executor when token is cancelled', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + let called = false + const p = new Promise((resolve, reject) => { + called = true + }, token) + assert(!called) + return assertSame(token.getRejected(), p) + }) + it('should reject if resolver throws synchronously', () => { const expected = new Error() return new Promise(() => { throw expected }) @@ -68,4 +80,70 @@ describe('Promise', () => { .then(assert.ifError, x => assert.strictEqual(expected, x)) }) }) + + describe('token', () => { + it('should immediately reject the promise when cancelled', () => { + const {token, cancel} = CancelToken.source() + const expected = new Error() + const p = new Promise(resolve => {}, token) + cancel(expected) + assert(isRejected(p)) + return p.then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + + it('should prevent otherwise fulfilling the promise after cancellation', () => { + const {token, cancel} = CancelToken.source() + const expected = new Error() + return new Promise(resolve => { + setTimeout(() => { + cancel(expected) + resolve(1) + }, 1) + }, token).then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + + it('should prevent otherwise rejecting the promise after cancellation', () => { + const {token, cancel} = CancelToken.source() + const expected = new Error() + return new Promise((resolve, reject) => { + setTimeout(() => { + cancel(expected) + reject(1) + }, 1) + }, token).then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + + it('should have no effect after fulfilling the promise', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + return new Promise(resolve => { + setTimeout(() => { + resolve(expected) + cancel(new Error()) + }, 1) + }, token).then(x => assert.strictEqual(expected, x)) + }) + + it('should have no effect after resolving the promise', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + return new Promise(resolve => { + setTimeout(() => { + resolve(new Promise(resolve => setTimeout(resolve, 1, expected))) + cancel(new Error()) + }, 1) + }, token).then(x => assert.strictEqual(expected, x)) + }) + + it('should have no effect after rejecting the promise', () => { + const {token, cancel} = CancelToken.source() + const expected = new Error() + return new Promise((_, reject) => { + setTimeout(() => { + reject(expected) + cancel(1) + }, 1) + }, token).then(assert.ifError, x => assert.strictEqual(expected, x)) + }) + }) }) diff --git a/test/cancellation-test.js b/test/cancellation-test.js new file mode 100644 index 0000000..0615f88 --- /dev/null +++ b/test/cancellation-test.js @@ -0,0 +1,1444 @@ +import { describe, it } from 'mocha' +import { future, reject, fulfill, never, isRejected, CancelToken } from '../src/main' +import { silenceError } from '../src/Promise' +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)) + +// cancellation tests for method calls on already settled or never-resolved promises +// with already cancelled or never cancelled tokens +// can be found in fulfill-test.js, reject-test-js and never-test.js + +describe('fulfill', () => { + describe('when being cancelled immediately after', () => { + describe('then', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).then(assert.ifError, null, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + /* describe('catch', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).catch(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) */ + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).map(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(assert.ifError).ap(fulfill(1), token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).chain(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after resolution just before the handler', () => { + describe('then', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + fulfill(1).then(() => cancel({})) + const res = fulfill(1).then(assert.ifError, null, token) + return assertSame(token.getRejected(), res) + }) + }) + + /* describe('catch', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + fulfill(1).then(() => cancel({})) + const res = fulfill(1).catch(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) */ + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + fulfill(1).then(() => cancel({})) + const res = fulfill(1).map(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + fulfill(1).then(() => cancel({})) + const res = fulfill(assert.ifError).ap(fulfill(1), token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + fulfill(1).then(() => cancel({})) + const res = fulfill(1).chain(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled from the handler', () => { + describe('then', () => { + it('should behave like cancellation and ignore exceptions for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).then(() => { + cancel({}) + throw new Error() + }, null, token) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).then(() => cancel({}), null, token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).map(() => cancel({}), token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(() => cancel({})).ap(fulfill(1), token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const res = fulfill(1).chain(() => cancel({}), token) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after the handler', () => { + describe('then', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.then(f, null, token) + promise.then(() => cancel({})) + return assertSame(promise.map(f), res) + }) + + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.then(fp, null, token) + promise.then(() => cancel({})) + return assertSame(promise.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.then(rp, null, token) + promise.then(() => cancel({})) + return assertSame(promise.chain(rp), res) + }) + }) + + /* describe('catch', () => { + it('should behave like fulfillment for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.catch(f, token) + promise.then(() => cancel({})) + return assertSame(promise, res) + }) + }) */ + + describe('map', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.map(f, token) + promise.then(() => cancel({})) + return assertSame(promise.map(f), res) + }) + }) + + describe('ap', () => { + it('should behave like apply for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(f) + const q = fulfill(1) + const res = promise.ap(q, token) + promise.ap(q).then(() => cancel({})) + return assertSame(promise.ap(q), res) + }) + }) + + describe('chain', () => { + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.chain(fp, token) + promise.then(() => cancel({})) + return assertSame(promise.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const promise = fulfill(1) + const res = promise.chain(rp, token) + promise.then(() => cancel({})) + return assertSame(promise.chain(rp), res) + }) + }) + }) +}) + +describe('reject', () => { + describe('when being cancelled immediately after', () => { + describe('catch', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).catch(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + /* describe('then', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).then(assert.ifError, null, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).map(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).ap(fulfill(1), token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).chain(assert.ifError, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) */ + }) + + describe('when being cancelled after resolution just before the handler', () => { + describe('catch', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + reject(1).catch(() => cancel({})) + const res = silenced(reject(1)).catch(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) + + /* describe('then', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + reject(1).catch(() => cancel({})) + const res = silenced(reject(1)).then(assert.ifError, null, token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + reject(1).catch(() => cancel({})) + const res = silenced(reject(1)).map(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + reject(1).catch(() => cancel({})) + const res = silenced(reject(1)).ap(fulfill(1), token) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + reject(1).catch(() => cancel({})) + const res = silenced(reject(1)).chain(assert.ifError, token) + return assertSame(token.getRejected(), res) + }) + }) */ + }) + + describe('when being cancelled from the handler', () => { + describe('catch', () => { + it('should behave like cancellation and ignore exceptions for reject', () => { + const { token, cancel } = CancelToken.source() + const res = reject(1).catch(() => { + cancel({}) + throw new Error() + }, token) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const res = silenced(reject(1)).catch(() => cancel({}), token) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after the handler', () => { + describe('catch', () => { + it('should behave like mapped for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = reject(1) + const res = promise.catch(f, token) + promise.catch(() => cancel({})) + return assertSame(promise.catch(f), res) + }) + + it('should behave like chained for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = reject(1) + const res = promise.catch(fp, token) + promise.catch(() => cancel({})) + return assertSame(promise.catch(fp), res) + }) + + it('should behave like rejection chained for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = reject(1) + const res = promise.catch(rp, token) + promise.catch(() => cancel({})) + return assertSame(promise.catch(rp), res) + }) + }) + + /* describe('then', () => { + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = silenced(reject(1)) + const res = promise.then(f, null, token) + promise.catch(() => cancel({})) + return assertSame(promise, res) + }) + }) + + describe('map', () => { + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = silenced(reject(1)) + const res = promise.map(f, token) + promise.catch(() => cancel({})) + return assertSame(promise, res) + }) + }) + + describe('ap', () => { + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = silenced(reject(f)) + const res = promise.ap(fulfill(1), token) + promise.catch(() => cancel({})) + return assertSame(promise, res) + }) + }) + + describe('chain', () => { + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const promise = silenced(reject(1)) + const res = promise.chain(fp, token) + promise.catch(() => cancel({})) + return assertSame(promise, res) + }) + }) */ + }) +}) + +describe('future', () => { + describe('then without callbacks', () => { + it('should behave like cancellation when cancelled', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.then(null, null, token) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation when cancelled for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(null, null, token) + resolve(never()) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like fulfillment when never cancelled for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(null, null, token) + resolve(p) + return assertSame(p, res) + }) + + it('should behave like rejection when never cancelled for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.then(null, null, token) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('when not being cancelled', () => { + describe('then', () => { + it('should behave like mapped for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(f, null, token) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like chained for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(fp, null, token) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(rp, null, token) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejection for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.then(f, null, token) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('catch', () => { + it('should behave like fulfillment for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.catch(f, token) + resolve(p) + return assertSame(p, res) + }) + + it('should behave like mapped for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(f, token) + resolve(p) + return assertSame(p.catch(f), res) + }) + + it('should behave like chained for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(fp, token) + resolve(p) + return assertSame(p.catch(fp), res) + }) + + it('should behave like rejection chained for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(rp, token) + resolve(p) + return assertSame(p.catch(rp), res) + }) + }) + + describe('map', () => { + it('should behave like mapped for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.map(f, token) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like rejection for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.map(f, token) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('ap', () => { + it('should behave like apply for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(f) + const q = fulfill(1) + const res = promise.ap(q, token) + resolve(p) + return assertSame(p.ap(q), res) + }) + + it('should behave like rejection for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(f)) + const res = promise.ap(fulfill(1), token) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('chain', () => { + it('should behave like chained for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(fp, token) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(rp, token) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejection for reject', () => { + const { token } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.chain(fp, token) + resolve(p) + return assertSame(p, res) + }) + }) + }) + + describe('when called with already cancelled token', () => { + describe('then', () => { + it('should return cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.then(assert.ifError, null, token) + resolve(fulfill(1)) + assert.strictEqual(token.getRejected(), res) + }) + + it('should return cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.then(assert.ifError, null, token) + resolve(silenced(reject(1))) + assert.strictEqual(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should return cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.catch(assert.ifError, token) + resolve(fulfill(1)) + assert.strictEqual(token.getRejected(), res) + }) + + it('should return cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.catch(assert.ifError, token) + resolve(silenced(reject(1))) + assert.strictEqual(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should return cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.map(assert.ifError, token) + resolve(fulfill(1)) + assert.strictEqual(token.getRejected(), res) + }) + + it('should return cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.map(assert.ifError, token) + resolve(silenced(reject(1))) + assert.strictEqual(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should return cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.ap(fulfill(1), token) + resolve(fulfill(assert.ifError)) + assert.strictEqual(token.getRejected(), res) + }) + + it('should return cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.ap(fulfill(1), token) + resolve(silenced(reject(1))) + assert.strictEqual(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should return cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.chain(assert.ifError, token) + resolve(fulfill(1)) + assert.strictEqual(token.getRejected(), res) + }) + + it('should return cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + cancel({}) + const res = promise.chain(assert.ifError, token) + resolve(silenced(reject(1))) + assert.strictEqual(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled and never resolved', () => { + describe('then', () => { + it('should behave like cancellation', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.then(assert.ifError, null, token) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(assert.ifError, null, token) + resolve(never()) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should behave like cancellation', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.catch(assert.ifError, token) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(assert.ifError, token) + resolve(never()) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.map(assert.ifError, token) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(assert.ifError, token) + resolve(never()) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.ap(fulfill(1), token) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + resolve(never()) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future() + const res = promise.chain(assert.ifError, token) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for never', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(assert.ifError, token) + resolve(never()) + cancel({}) + assert(isRejected(res)) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled before resolution', () => { + describe('then', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(assert.ifError, null, token) + cancel({}) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(assert.ifError, null, token) + cancel({}) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(assert.ifError, token) + cancel({}) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(assert.ifError, token) + cancel({}) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(assert.ifError, token) + cancel({}) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(assert.ifError, token) + cancel({}) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + cancel({}) + resolve(fulfill(assert.ifError)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + cancel({}) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(assert.ifError, token) + cancel({}) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(assert.ifError, token) + cancel({}) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after resolution', () => { + describe('then', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(assert.ifError, null, token) + resolve(fulfill(1)) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(assert.ifError, null, token) + resolve(silenced(reject(1))) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(assert.ifError, token) + resolve(fulfill(1)) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(assert.ifError, token) + resolve(silenced(reject(1))) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(assert.ifError, token) + resolve(fulfill(1)) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(assert.ifError, token) + resolve(silenced(reject(1))) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + resolve(fulfill(assert.ifError)) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + resolve(silenced(reject(1))) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(assert.ifError, token) + resolve(fulfill(1)) + cancel({}) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(assert.ifError, token) + resolve(silenced(reject(1))) + cancel({}) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after resolution just before the handler', () => { + describe('then', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.then(() => cancel({})) + const res = promise.then(assert.ifError, null, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.catch(() => cancel({})) + const res = promise.then(assert.ifError, null, token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.then(() => cancel({})) + const res = promise.catch(assert.ifError, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.catch(() => cancel({})) + const res = promise.catch(assert.ifError, token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.then(() => cancel({})) + const res = promise.map(assert.ifError, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.catch(() => cancel({})) + const res = promise.map(assert.ifError, token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.then(() => cancel({})) + const res = promise.ap(fulfill(1), token) + resolve(fulfill(assert.ifError)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.catch(() => cancel({})) + const res = promise.ap(fulfill(1), token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.then(() => cancel({})) + const res = promise.chain(assert.ifError, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + promise.catch(() => cancel({})) + const res = promise.chain(assert.ifError, token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled from the handler', () => { + /* alternative proposal + const pre = (f, g) => x => (f({}), g(x)) + describe('then', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(pre(cancel, f), null, token) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(pre(cancel, fp), null, token) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(pre(cancel, rp), null, token) + resolve(p) + return assertSame(p.chain(rp), res) + }) + }) + + describe('catch', () => { + it('should behave like mapped for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(pre(cancel, f), token) + resolve(p) + return assertSame(p.catch(f), res) + }) + + it('should behave like chained for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(pre(cancel, fp), token) + resolve(p) + return assertSame(p.catch(fp), res) + }) + + it('should behave like rejection chained for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(pre(cancel, rp), token) + resolve(p) + return assertSame(p.catch(rp), res) + }) + }) + + describe('map', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.map(pre(cancel, f), token) + resolve(p) + return assertSame(p.map(f), res) + }) + }) + + describe('ap', () => { + it('should behave like apply for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(pre(cancel, f)) + const q = fulfill(1) + const res = promise.ap(q, token) + resolve(p) + return assertSame(fulfill(f).ap(q), res) + }) + }) + + describe('chain', () => { + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(pre(cancel, fp), token) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(pre(cancel, rp), token) + resolve(p) + return assertSame(p.chain(rp), res) + }) + }) */ + + describe('then', () => { + it('should behave like cancellation and ignore exceptions for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(() => { + cancel({}) + throw new Error() + }, null, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.then(() => cancel({}), null, token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('catch', () => { + it('should behave like cancellation for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.catch(() => cancel({}), token) + resolve(silenced(reject(1))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('map', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.map(() => cancel({}), token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + }) + + describe('ap', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.ap(fulfill(1), token) + resolve(fulfill(() => cancel({}))) + return assertSame(token.getRejected(), res) + }) + }) + + describe('chain', () => { + it('should behave like cancellation for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const res = promise.chain(() => cancel({}), token) + resolve(fulfill(1)) + return assertSame(token.getRejected(), res) + }) + }) + }) + + describe('when being cancelled after the handler', () => { + describe('then', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(f, null, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(fp, null, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.then(rp, null, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.then(f, null, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('catch', () => { + it('should behave like fulfillment for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.catch(f, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p, res) + }) + + it('should behave like mapped for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(f, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p.catch(f), res) + }) + + it('should behave like chained for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(fp, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p.catch(fp), res) + }) + + it('should behave like rejection chained for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = reject(1) + const res = promise.catch(rp, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p.catch(rp), res) + }) + }) + + describe('map', () => { + it('should behave like mapped for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.map(f, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.map(f), res) + }) + + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.map(f, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('ap', () => { + it('should behave like apply for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(f) + const q = fulfill(1) + const res = promise.ap(q, token) + promise.ap(q).then(() => cancel({})) + resolve(p) + return assertSame(p.ap(q), res) + }) + + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(f)) + const res = promise.ap(fulfill(1), token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p, res) + }) + }) + + describe('chain', () => { + it('should behave like chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(fp, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.chain(fp), res) + }) + + it('should behave like rejection chained for fulfill', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = fulfill(1) + const res = promise.chain(rp, token) + promise.then(() => cancel({})) + resolve(p) + return assertSame(p.chain(rp), res) + }) + + it('should behave like rejection for reject', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future() + const p = silenced(reject(1)) + const res = promise.chain(fp, token) + promise.catch(() => cancel({})) + resolve(p) + return assertSame(p, res) + }) + }) + }) +}) diff --git a/test/delay-test.js b/test/delay-test.js index 84ba2ec..3e646ab 100644 --- a/test/delay-test.js +++ b/test/delay-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { delay, never, reject, fulfill, isNever, isPending } from '../src/main' +import { delay, never, reject, fulfill, isRejected, isNever, isPending, CancelToken } from '../src/main' import { Future, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -53,4 +53,64 @@ describe('delay', function () { return assertSame(fulfill(x), p) .then(() => assert(lte(t, Date.now() - now))) }) + + it('should delay fulfilled when never cancelled', () => { + const x = {} + const t = 10 + const p = delay(t, fulfill(x), CancelToken.empty()) + + const now = Date.now() + return assertSame(fulfill(x), p) + .then(() => assert(lte(t, Date.now() - now))) + }) + + it('should return cancellation with cancelled token for fulfill', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + const p = delay(10, fulfill(1), token) + assert.strictEqual(token.getRejected(), p) + }) + + it('should return cancellation with cancelled token for reject', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + const p = delay(10, reject(1), token) + assert.strictEqual(token.getRejected(), p) + }) + + it('should behave like cancellation when cancelled for never', () => { + const {token, cancel} = CancelToken.source() + const p = delay(10, never(), token) + cancel({}) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + + it('should behave like cancellation when cancelled', () => { + const {token, cancel} = CancelToken.source() + const p = delay(10, fulfill(1), token) + cancel({}) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + + it('should behave like cancellation when cancelled during delay', () => { + const {token, cancel} = CancelToken.source() + const p = delay(10, fulfill(1), token) + return delay(5).then(() => { + cancel({}) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + }) + + it('should behave like cancellation when cancelled before fulfill', () => { + const {token, cancel} = CancelToken.source() + const p = delay(5, delay(10), token) + return delay(5).then(() => { + cancel({}) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + }) }) From e61b095eaf0ed278d07b2d1bebef6746997768e2 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 29 Jun 2016 06:52:26 +0200 Subject: [PATCH 15/28] cancellable coroutines complete with small test suite and full coverage --- src/CancelToken.js | 7 +- src/coroutine.js | 137 ++++++++++++++++++++++++++++---- src/main.js | 20 +---- test/coroutine-test.js | 174 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 298 insertions(+), 40 deletions(-) diff --git a/src/CancelToken.js b/src/CancelToken.js index 088dcb3..8559fb6 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -115,10 +115,9 @@ export default class CancelToken { return new this(cancel => resolve(thenable).then(cancel)) // finally? } static from (cancelTokenlike) { - /* istanbul ignore else */ - if (cancelTokenlike instanceof CancelToken) { - return cancelTokenlike - } + // if (cancelTokenlike == null) return null + if (cancelTokenlike instanceof CancelToken) return cancelTokenlike + return null } static empty () { return new this(noop) // NeverCancelToken diff --git a/src/coroutine.js b/src/coroutine.js index e9b9be2..2b090d5 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -1,37 +1,140 @@ -import { resolve } from './Promise' +import { Future, resolve, reject } from './Promise' +import CancelToken from './CancelToken' import Action from './Action' -export default function coroutine (iterator, promise) { - new Coroutine(iterator, promise).run() - // taskQueue.add(new Coroutine(iterator, promise)) +// ------------------------------------------------------------- +// ## Coroutine +// ------------------------------------------------------------- + +// coroutine :: Generator e a -> (...* -> Promise e a) +// Make a coroutine from a promise-yielding generator +export default function coroutine (generatorFunction) { + return function coroutinified () { + return runGenerator(generatorFunction.apply(this, arguments)) + } +} + +const stack = [] +Object.defineProperty(coroutine, 'cancel', { + get () { + if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') + return stack[stack.length - 1].curToken + }, + set (token) { + if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') + token = CancelToken.from(token) + stack[stack.length - 1].setToken(token) + }, + configurable: true +}) + +function runGenerator (generator) { + const promise = new Future() + new Coroutine(generator, promise).run() + // taskQueue.add(new Coroutine(generator, promise)) return promise } class Coroutine extends Action { - constructor (iterator, promise) { + constructor (generator, promise) { super(promise) - this.next = iterator.next.bind(iterator) - this.throw = iterator.throw.bind(iterator) + // the generator that is driven. After cancellation, reference to cleanup coroutine + this.generator = generator + // the CancelToken (or null) currently associated with this.promise + this.curToken = null } run () { - this.tryCall(this.next, void 0) + this.step(this.generator.next, void 0) + } + + fulfilled (ref) { + this.step(this.generator.next, ref.value) + } + + rejected (ref) { + this.step(this.generator.throw, ref.value) + return true } - handle (result) { - if (result.done) { - return this.promise._resolve(result.value) + cancel (p) { + super.cancel(p) + /* istanbul ignore else */ + if (!this.promise) { // action got destroyed + const res = this.initCancel(p) + if (stack.indexOf(this) < 0) { + this.resumeCancel() + } + return res } + } - resolve(result.value)._runAction(this) + step (f, x) { + /* eslint complexity:[2,4] */ + let result + stack.push(this) + try { + result = f.call(this.generator, x) + } catch (e) { + result = {value: reject(e), done: true} + } finally { + stack.pop() // assert: === this + } + if (this.promise) { + if (result.done) { + this.promise._resolve(result.value) + } else { + resolve(result.value)._runAction(this) + } + } else { // cancelled during execution + // ignoring result.done and result.value + // if done, one would only need to resolve the initialised promise and not call return() + this.resumeCancel() + } } - fulfilled (ref) { - this.tryCall(this.next, ref.value) + initCancel (p) { + // assert: p === this.curToken.getRejected() + const promise = new Future() + const cancelRoutine = new Coroutine(this.generator, promise) + cancelRoutine.curToken = p.value + this.generator = cancelRoutine + return promise } - rejected (ref) { - this.tryCall(this.throw, ref.value) - return true + resumeCancel () { + const cancelRoutine = this.generator + this.generator = null + const reason = cancelRoutine.curToken + cancelRoutine.curToken = null + // assert: reason === this.curToken.getRejected().value + this.curToken = null + cancelRoutine.step(cancelRoutine.generator.return, reason) + } + + setToken (newToken) { + /* eslint complexity:[2,6] */ + const oldToken = this.curToken + if (oldToken && oldToken.requested) { + throw new ReferenceError('coroutine.cancel must not be changed after being cancelled') + } + if (oldToken !== newToken) { + const p = this.promise + if (oldToken) { + oldToken._unsubscribe(this) // BUG: unsubscribing can destroy the action + this.promise = p // we don't want that and restore it - cancel() might still be called + // but that doesn't have an effect when newToken isn't cancelled + } + this.curToken = p.token = newToken + if (newToken) { + if (newToken.requested) { + const r = newToken.getRejected() + super.cancel(r) + this.initCancel(r) + } else { + newToken._subscribe(this) + } + } + } } } diff --git a/src/main.js b/src/main.js index 0c788f8..62788ab 100644 --- a/src/main.js +++ b/src/main.js @@ -12,6 +12,8 @@ import { all, race } from './combinators' export { default as CancelToken } from './CancelToken' +export { default as coroutine } from './coroutine.js' + import Action from './Action' import _delay from './delay' @@ -19,24 +21,6 @@ import _timeout from './timeout' import _runPromise from './runPromise' import _runNode from './node' -import _runCoroutine from './coroutine.js' - -// ------------------------------------------------------------- -// ## Coroutine -// ------------------------------------------------------------- - -// coroutine :: Generator e a -> (...* -> Promise e a) -// Make a coroutine from a promise-yielding generator -export function coroutine (generator) { - return function coroutinified (...args) { - return runGenerator(generator, this, args) - } -} - -function runGenerator (generator, thisArg, args) { - const iterator = generator.apply(thisArg, args) - return _runCoroutine(iterator, new Future()) -} // ------------------------------------------------------------- // ## Node-style async diff --git a/test/coroutine-test.js b/test/coroutine-test.js index bae18d5..f6f6647 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -1,5 +1,6 @@ import { describe, it } from 'mocha' -import { coroutine, fulfill, reject, delay } from '../src/main' +import { coroutine, fulfill, reject, delay, isRejected, CancelToken } from '../src/main' +import { assertSame } from './lib/test-util' import assert from 'assert' describe('coroutine', function () { @@ -43,4 +44,175 @@ describe('coroutine', function () { return f(expected) .then(assert.ifError, e => assert.strictEqual(e, expected)) }) + + describe('cancellation', () => { + it('should receive a token cancelled outside', () => { + let executed = false + const f = coroutine(function* (token) { + coroutine.cancel = token + yield delay(5) + executed = true + return 1 + }) + const {token, cancel} = CancelToken.source() + const expected = {} + delay(3, expected).then(cancel) + return f(token).then(assert.ifError, x => { + assert.strictEqual(x, expected) + return delay(5).then(() => assert(!executed)) + }) + }) + + it('should execute finally but not catch statements', () => { + let executedT = false + let executedC = false + let executedF = false + const f = coroutine(function* (token) { + coroutine.cancel = token + try { + yield delay(5) + executedT = true + } catch (e) { + executedC = true + } finally { + executedF = true + } + return 1 + }) + const {token, cancel} = CancelToken.source() + f(token) + return delay(3, {}).then(cancel).then(() => { + assert(!executedT, 'after yield') + assert(!executedC, 'catch block') + assert(executedF, 'finally block') + }) + }) + + it('should wait on yields in finally statements', () => { + let executed = false + const f = coroutine(function* (token) { + coroutine.cancel = token + try { + yield delay(5) + } finally { + yield delay(2) + executed = true + } + return 1 + }) + const {token, cancel} = CancelToken.source() + const p = f(token) + const d = delay(3, {}).then(() => { + cancel() + assert(isRejected(p)) + assert(!executed, 'at yield') + }) + return delay(5, d).then(() => assert(executed, 'after yield in finally block')) + }) + + it('should receive a token cancelled inside', () => { + const expected = {} + let rejected = false + let executedT = false + let executedF = false + const f = coroutine(function* () { + const {token, cancel} = CancelToken.source() + coroutine.cancel = token + yield delay(1) + try { + cancel(expected) + rejected = isRejected(p) + yield + executedT = true + } finally { + executedF = true + } + }) + const p = f() + return p.then(assert.ifError, x => { + assert(rejected, 'immediately rejected') + assert.strictEqual(x, expected) + assert(!executedT, 'after yield') + assert(executedF, 'finally block') + }) + }) + + it('should cancel when receiving a cancelled token', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + const f = coroutine(function* () { + coroutine.cancel = token + }) + return assertSame(f(), token.getRejected()) + }) + + it('should not cancel when the last received token is not cancelled', () => { + return coroutine(function* () { + const {token, cancel} = CancelToken.source() + coroutine.cancel = token + yield + coroutine.cancel = null + cancel({}) + return 1 + })().then(x => assert.strictEqual(x, 1)) + }) + + it('should work for recursive coroutines', () => { + let counter = 0 + const f = coroutine(function* (token) { + coroutine.cancel = token + yield delay(1) + try { + counter++ + yield f(token) + } finally { + counter-- + } + }) + const {token, cancel} = CancelToken.source() + const p = f(token) + return delay(15).then(() => { + cancel({}) + assert(isRejected(p)) + assert(counter == 0) + }) + }) + }) + + describe('coroutine.cancel', () => { + it('should behave like assignment', () => { + return coroutine(function* () { + const token = CancelToken.empty() + coroutine.cancel = token + assert.strictEqual(coroutine.cancel, token) + coroutine.cancel = token + coroutine.cancel = null + assert.strictEqual(coroutine.cancel, null) + coroutine.cancel = null + })() + }) + + it('should throw when used outside a coroutine', () => { + assert.throws(() => coroutine.cancel, SyntaxError) + assert.throws(() => { coroutine.cancel = null }, SyntaxError) + }) + + it('should throw when assigned to after cancellation', () => { + let err + const p = coroutine(function* () { + const {token, cancel} = CancelToken.source() + coroutine.cancel = token + yield delay(1) + cancel() + try { + assert(isRejected(p)) + assert.throws(() => { coroutine.cancel = null }, ReferenceError) + assert.throws(() => { coroutine.cancel = CancelToken.empty() }, ReferenceError) + } catch (e) { + err = e + } + })() + return p.then(assert.ifError, () => assert.ifError(err)) + }) + }) }) From d09a447aaed64c955d9724e58192be981d1b9cd5 Mon Sep 17 00:00:00 2001 From: Bergi Date: Sun, 3 Jul 2016 23:18:23 +0200 Subject: [PATCH 16/28] cancellation for assimilated thenables and promises --- src/Action.js | 1 + src/CancelToken.js | 5 ++-- src/Promise.js | 57 +++++++++++++++++++++++++++++++++------- test/Promise-test.js | 22 ++++++++-------- test/resolve-test.js | 62 +++++++++++++++++++++++++++++++++++++++----- 5 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/Action.js b/src/Action.js index 178f4fb..56afe23 100644 --- a/src/Action.js +++ b/src/Action.js @@ -4,6 +4,7 @@ export default class Action { // when null, the action is cancelled and won't be executed const token = promise.token if (token != null) { + // assert: !token.requested token._subscribe(this) } } diff --git a/src/CancelToken.js b/src/CancelToken.js index 8559fb6..7d76ec2 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -115,9 +115,10 @@ export default class CancelToken { return new this(cancel => resolve(thenable).then(cancel)) // finally? } static from (cancelTokenlike) { - // if (cancelTokenlike == null) return null + if (cancelTokenlike == null) return null + /* istanbul ignore else */ if (cancelTokenlike instanceof CancelToken) return cancelTokenlike - return null + else throw new TypeError('not a CancelToken') // TODO } static empty () { return new this(noop) // NeverCancelToken diff --git a/src/Promise.js b/src/Promise.js index dc541c0..8dd2675 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -100,6 +100,23 @@ export class Future extends Core { : race([n, bp]) } + // untilCancel :: Promise e a -> CancelToken e -> Promise e a + untilCancel (token) { + /* eslint complexity:[2,5] */ + const n = this.near() + if (n !== this) { + return n.untilCancel(token) + } else if (token == null || token === this.token) { + return this + } + const p = new Future(token) + if (p.token.requested) { + return p.token.getRejected() + } + this._runAction(new Action(p)) + return p + } + // toString :: Promise e a -> String toString () { return '[object ' + this.inspect() + ']' @@ -148,7 +165,7 @@ export class Future extends Core { } _resolve (x) { - this._become(resolve(x)) + this._become(resolve(x, this.token)) } _fulfill (x) { @@ -230,6 +247,10 @@ class Fulfilled extends Core { return this } + untilCancel (token) { + return rejectedIfCancelled(token, this) + } + toString () { return '[object ' + this.inspect() + ']' } @@ -290,6 +311,10 @@ class Rejected extends Core { return this } + untilCancel (token) { + return rejectedIfCancelled(token, this) + } + toString () { return '[object ' + this.inspect() + ']' } @@ -345,6 +370,10 @@ class Never extends Core { return b } + untilCancel (token) { + return rejectedWhenCancel(token, this) + } + toString () { return '[object ' + this.inspect() + ']' } @@ -382,16 +411,24 @@ export function silenceError (p) { // ## Creating promises // ------------------------------------------------------------- +// resolve :: Thenable e a -> CancelToken e -> Promise e a // resolve :: Thenable e a -> Promise e a // resolve :: a -> Promise e a -export function resolve (x) { - return isPromise(x) ? x.near() - : isObject(x) ? refForMaybeThenable(x) - : new Fulfilled(x) +export function resolve (x, token) { + /* eslint complexity:[2,6] */ + if (isPromise(x)) { + return x.untilCancel(token) + } else if (token != null && token.requested) { + return token.getRejected() + } else if (isObject(x)) { + return refForMaybeThenable(x, token) + } else { + return new Fulfilled(x) + } } export function resolveObject (o) { - return isPromise(o) ? o.near() : refForMaybeThenable(o) + return isPromise(o) ? o.near() : refForMaybeThenable(o, null) } // reject :: e -> Promise e a @@ -436,11 +473,11 @@ function rejectedWhenCancel (token, never) { return CancelToken.from(token).getRejected() } -function refForMaybeThenable (x) { +function refForMaybeThenable (x, token) { try { const then = x.then return typeof then === 'function' - ? extractThenable(then, x) + ? extractThenable(then, x, token) : fulfill(x) } catch (e) { return new Rejected(e) @@ -448,11 +485,11 @@ function refForMaybeThenable (x) { } // WARNING: Naming the first arg "then" triggers babel compilation bug -function extractThenable (thn, thenable) { +function extractThenable (thn, thenable, token) { const p = new Future() try { - thn.call(thenable, x => p._resolve(x), e => p._reject(e)) + thn.call(thenable, x => p._resolve(x), e => p._reject(e), token) } catch (e) { p._reject(e) } diff --git a/test/Promise-test.js b/test/Promise-test.js index 5149c00..f3ed0ad 100644 --- a/test/Promise-test.js +++ b/test/Promise-test.js @@ -124,17 +124,6 @@ describe('Promise', () => { }, token).then(x => assert.strictEqual(expected, x)) }) - it('should have no effect after resolving the promise', () => { - const {token, cancel} = CancelToken.source() - const expected = {} - return new Promise(resolve => { - setTimeout(() => { - resolve(new Promise(resolve => setTimeout(resolve, 1, expected))) - cancel(new Error()) - }, 1) - }, token).then(x => assert.strictEqual(expected, x)) - }) - it('should have no effect after rejecting the promise', () => { const {token, cancel} = CancelToken.source() const expected = new Error() @@ -145,5 +134,16 @@ describe('Promise', () => { }, 1) }, token).then(assert.ifError, x => assert.strictEqual(expected, x)) }) + + it('should still reject the promise after resolving the promise without settling it', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + return new Promise(resolve => { + setTimeout(() => { + resolve(new Promise(resolve => setTimeout(resolve, 1))) + cancel(expected) + }, 1) + }, token).then(assert.ifError, x => assert.strictEqual(expected, x)) + }) }) }) diff --git a/test/resolve-test.js b/test/resolve-test.js index addfac5..12f93ab 100644 --- a/test/resolve-test.js +++ b/test/resolve-test.js @@ -1,42 +1,90 @@ import { describe, it } from 'mocha' -import { resolve } from '../src/main' +import { resolve, CancelToken } from '../src/main' import { Future } from '../src/Promise' import assert from 'assert' describe('resolve', () => { it('should reject promise cycle', () => { - let p = new Future() + const p = new Future() p._resolve(p) return p.then(assert.ifError, e => assert(e instanceof TypeError)) }) + it('should reject indirect promise cycle', () => { + const p1 = new Future() + const p2 = new Future() + p1._resolve(p2) + p2._resolve(p1) + return p1.then(assert.ifError, e => assert(e instanceof TypeError)) + }) + describe('thenables', () => { it('should resolve fulfilled thenable', () => { - let expected = {} + const expected = {} return resolve({ then: f => f(expected) }) .then(x => assert.strictEqual(expected, x)) }) it('should resolve rejected thenable', () => { - let expected = {} + const expected = {} return resolve({ then: (f, r) => r(expected) }) .then(assert.ifError, e => assert.strictEqual(expected, e)) }) it('should reject if thenable.then throws', () => { - let expected = {} + const expected = {} return resolve({ then: () => { throw expected } }) .then(assert.ifError, e => assert.strictEqual(expected, e)) }) it('should reject if accessing thenable.then throws', () => { - let expected = {} - let thenable = { + const expected = {} + const thenable = { get then () { throw expected } } return resolve(thenable) .then(assert.ifError, e => assert.strictEqual(expected, e)) }) + + it('should receive a token', () => { + const {token} = CancelToken.source() + return resolve({ + then: (f, r, t) => { + assert.strictEqual(t, token) + f() + } + }, token) + }) + + it('should receive the token of the future it resolves', () => { + const {token} = CancelToken.source() + const p = new Future(token) + p._resolve({ + then: (f, r, t) => { + assert.strictEqual(t, token) + f() + } + }) + return p + }) + + it('should return cancellation with cancelled token for true', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + assert.strictEqual(token.getRejected(), resolve(true, token)) + }) + + it('should return cancellation with cancelled token for future', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + assert.strictEqual(token.getRejected(), resolve(new Future(), token)) + }) + + it('should be identity for future with same token', () => { + const {token} = CancelToken.source() + const p = new Future(token) + assert.strictEqual(p, resolve(p, token)) + }) }) }) From aac0f8e4911b778de6fadf8b904428bafd2b4c1b Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 5 Jul 2016 01:05:03 +0200 Subject: [PATCH 17/28] make .token public and immutable required some work especially for coroutines --- src/Promise.js | 1 - src/coroutine.js | 87 ++++++++++++++++++++++++++++-------------- test/coroutine-test.js | 34 ++++++++++++++--- 3 files changed, 87 insertions(+), 35 deletions(-) diff --git a/src/Promise.js b/src/Promise.js index 8dd2675..e04692c 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -190,7 +190,6 @@ export class Future extends Core { __become (p) { this.ref = p === this ? cycle() : p - this.token = null if (this.action === void 0) { return diff --git a/src/coroutine.js b/src/coroutine.js index 2b090d5..e222189 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -1,3 +1,4 @@ +import { noop } from './util' import { Future, resolve, reject } from './Promise' import CancelToken from './CancelToken' import Action from './Action' @@ -18,18 +19,18 @@ const stack = [] Object.defineProperty(coroutine, 'cancel', { get () { if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') - return stack[stack.length - 1].curToken + return stack[stack.length - 1].token }, set (token) { if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') token = CancelToken.from(token) - stack[stack.length - 1].setToken(token) + stack[stack.length - 1].token._follow(token) }, configurable: true }) function runGenerator (generator) { - const promise = new Future() + const promise = new Future(new SwappableCancelToken()) new Coroutine(generator, promise).run() // taskQueue.add(new Coroutine(generator, promise)) return promise @@ -40,8 +41,14 @@ class Coroutine extends Action { super(promise) // the generator that is driven. After cancellation, reference to cleanup coroutine this.generator = generator - // the CancelToken (or null) currently associated with this.promise - this.curToken = null + // the CancelToken that can be directed to follow the current token + this.token = promise.token + } + + destroy () { + super.destroy() + this.generator = null + this.token = null } run () { @@ -58,9 +65,9 @@ class Coroutine extends Action { } cancel (p) { - super.cancel(p) /* istanbul ignore else */ - if (!this.promise) { // action got destroyed + if (this.promise._isResolved()) { // promise checks for cancellation itself + this.promise = null const res = this.initCancel(p) if (stack.indexOf(this) < 0) { this.resumeCancel() @@ -81,10 +88,12 @@ class Coroutine extends Action { stack.pop() // assert: === this } if (this.promise) { + const token = this.promise.token if (result.done) { this.promise._resolve(result.value) + if (token != null) token._unsubscribe(this) } else { - resolve(result.value)._runAction(this) + resolve(result.value, token)._runAction(this) } } else { // cancelled during execution // ignoring result.done and result.value @@ -94,43 +103,65 @@ class Coroutine extends Action { } initCancel (p) { - // assert: p === this.curToken.getRejected() + // assert: p === this.promise.token.getRejected() const promise = new Future() - const cancelRoutine = new Coroutine(this.generator, promise) - cancelRoutine.curToken = p.value - this.generator = cancelRoutine + this.generator = new Coroutine(this.generator, promise, p.value) return promise } resumeCancel () { const cancelRoutine = this.generator this.generator = null - const reason = cancelRoutine.curToken - cancelRoutine.curToken = null - // assert: reason === this.curToken.getRejected().value - this.curToken = null + const reason = cancelRoutine.token + cancelRoutine.token = null // not cancellable + // assert: reason === this.token.getRejected().value + this.token = null cancelRoutine.step(cancelRoutine.generator.return, reason) } +} + +class SwappableCancelToken extends CancelToken { // also implements cancel parts of Action + constructor () { + super(noop) + this.promise = new Future() + this.curToken = null + } + + destroy () { + // possibly called when unsubscribed from curToken + } + + cancel (p) { + if (this._cancelled) return + if (p !== this.curToken.getRejected()) return + this._cancelled = true + this.promise._resolve(p) + return this.run() + } + + get requested () { + if (this.curToken == null) return false + const c = this.curToken.requested + if (c && !this._cancelled) { + this.cancel(this.curToken.getRejected()) + } + return c + } - setToken (newToken) { - /* eslint complexity:[2,6] */ + _follow (newToken) { + /* eslint complexity:[2,7] */ const oldToken = this.curToken if (oldToken && oldToken.requested) { - throw new ReferenceError('coroutine.cancel must not be changed after being cancelled') + throw new ReferenceError('token must not be changed after being cancelled') } - if (oldToken !== newToken) { - const p = this.promise + if (oldToken !== newToken && this !== newToken) { if (oldToken) { - oldToken._unsubscribe(this) // BUG: unsubscribing can destroy the action - this.promise = p // we don't want that and restore it - cancel() might still be called - // but that doesn't have an effect when newToken isn't cancelled + oldToken._unsubscribe(this) } - this.curToken = p.token = newToken + this.curToken = newToken if (newToken) { if (newToken.requested) { - const r = newToken.getRejected() - super.cancel(r) - this.initCancel(r) + this.cancel(newToken.getRejected()) } else { newToken._subscribe(this) } diff --git a/test/coroutine-test.js b/test/coroutine-test.js index f6f6647..529d92e 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -180,15 +180,37 @@ describe('coroutine', function () { }) describe('coroutine.cancel', () => { - it('should behave like assignment', () => { - return coroutine(function* () { - const token = CancelToken.empty() + it('should behave like the last assigned token', () => { + const {token, cancel} = CancelToken.source() + let c_token + const p = coroutine(function* () { + assert(!coroutine.cancel.requested) + coroutine.cancel = CancelToken.empty() + assert(!coroutine.cancel.requested) + coroutine.cancel = null + assert(!coroutine.cancel.requested) coroutine.cancel = token + assert(!coroutine.cancel.requested) + cancel({}) + assert(coroutine.cancel.requested) + c_token = coroutine.cancel + })() + return p.then(assert.ifError, e => { + assert.strictEqual(c_token, p.token) + return assertSame(token.getRejected(), c_token.getRejected()) + }) + }) + + it('should always return the same token', () => { + return coroutine(function* () { + const token = coroutine.cancel + assert.strictEqual(coroutine.cancel, token) + coroutine.cancel = CancelToken.empty() assert.strictEqual(coroutine.cancel, token) - coroutine.cancel = token - coroutine.cancel = null - assert.strictEqual(coroutine.cancel, null) coroutine.cancel = null + assert.strictEqual(coroutine.cancel, token) + coroutine.cancel = coroutine.cancel + assert.strictEqual(coroutine.cancel, token) })() }) From b70fe8fea7bc7fb72588686905344bb945d5cfa0 Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 5 Jul 2016 19:06:45 +0200 Subject: [PATCH 18/28] sanity improvements for CancelTokenPool and CancelTokenReference * factored SwappableCancelToken out of coroutine.js * made .requested work as expected on LiveCancelTokens * added tests for all of those * fixed return values of .cancel() --- src/CancelToken.js | 153 ++++++++++++++++++++++++++++++++------- src/Promise.js | 10 ++- src/coroutine.js | 97 ++++++------------------- test/CancelToken-test.js | 82 ++++++++++++++++++++- test/coroutine-test.js | 53 ++++++++------ 5 files changed, 270 insertions(+), 125 deletions(-) diff --git a/src/CancelToken.js b/src/CancelToken.js index 7d76ec2..cc1765e 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,5 +1,5 @@ import { noop } from './util' -import { Future, resolve, reject, never, silenceError, taskQueue } from './Promise' // deferred +import { Future, resolve, reject, silentReject, never, taskQueue } from './Promise' // deferred import { isSettled } from './inspect' export default class CancelToken { @@ -19,9 +19,10 @@ export default class CancelToken { } _cancel (reason) { if (this._cancelled) return + return this.__cancel(silentReject(reason)) + } + __cancel (p) { this._cancelled = true - const p = reject(reason) // tag as intentionally rejected, p._state |= CANCELLED? - silenceError(p) if (this.promise !== void 0) { this.promise._resolve(p) } else { @@ -33,18 +34,28 @@ export default class CancelToken { /* eslint complexity:[2,4] */ const result = [] for (let i = 0; i < this.length; ++i) { - try { - if (this[i] && this[i].promise) { // not already destroyed - result.push(resolve(this[i].cancel(this.promise))) - } - } catch (e) { - result.push(reject(e)) + if (this[i] && this[i].promise) { // not already destroyed + this._runAction(this[i], result) } this[i] = void 0 } this.length = 0 return result } + _runAction (action, results) { + try { + const res = action.cancel(this.promise) + if (res != null) { + if (Array.isArray(res)) { + results.push(...res) + } else { + results.push(res) + } + } + } catch (e) { + results.push(reject(e)) + } + } _subscribe (action) { if (this.requested && this.length === 0) { taskQueue.add(this) // asynchronous? @@ -53,7 +64,8 @@ export default class CancelToken { } _unsubscribe (action) { /* eslint complexity:[2,6] */ - for (let i = Math.min(5, this.length); i--;) { + let i = this._cancelled ? 0 : Math.min(5, this.length) + while (i--) { // an inplace-filtering algorithm to remove empty actions // executed at up to 5 steps per unsubscribe if (this.scanHigh < this.length) { @@ -80,7 +92,7 @@ export default class CancelToken { promise, cancel (p) { if (!isSettled(this.promise)) { - return fn(p.value) + return resolve(fn(p.near().value)) } } }) @@ -103,7 +115,7 @@ export default class CancelToken { const token = new this(noop) return { token, - cancel (r) { token._cancel(r) } + cancel (r) { return token._cancel(r) } } } else { let cancel @@ -134,32 +146,121 @@ export default class CancelToken { static pool (tokens) { return new CancelTokenPool(tokens) } + static reference (cur) { + return new CancelTokenReference(cur) + } } -class CancelTokenPool { +class LiveCancelToken extends CancelToken { + constructor (check) { + super(noop) + this.check = check + } + __cancel (p) { + this.check = null + return super.__cancel(p) + } + get requested () { + return this._cancelled || this.check._testRequested() + /* if (this._cancelled) return true + const c = this.check._testRequested() + if (c) { + this.__cancel(this.check._getRejected()) + } + return c */ + } +} + +class CancelTokenPool { // implements cancel parts of Action constructor (tokens) { - this.token = new CancelToken(noop) - this.reasons = [] + this.promise = new LiveCancelToken(this) + this.tokens = [] this.count = 0 - this.check = r => { - this.reasons.push(r) - if (--this.count === 0) { - this.token._cancel(this.reasons) // forward return value ??? - this.reasons = null - } - } if (tokens) this.add(...tokens) - // if (this.count === 0 && !this.token.requested) this.token._cancel() ??? + } + // never called (by unsubscribe): destroy () {} + cancel (p) { + // assert: !this.promise._cancelled + if (--this.count === 0) { + return this.promise.__cancel(this._getRejected()) + } + } + _testRequested () { + return this.tokens.length > 0 && this.tokens.every(t => t.requested) + } + _getRejected () { + const reasons = this.tokens.map(t => t.getRejected().near().value) + this.tokens = null + return silentReject(reasons) } add (...tokens) { - if (this.token.requested) return + if (this.tokens == null) return this.count += tokens.length // for (let t of tokens) { // https://phabricator.babeljs.io/T2164 for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) { - CancelToken.from(t).subscribe(this.check) + t = CancelToken.from(t) + if (this.promise === t) { + this.count-- + continue + } + this.tokens.push(t) + if (t.requested) { + this.count-- + } else { + t._subscribe(this) + } + } + if (this.tokens.length > 0 && this.count === 0) { + this.promise.__cancel(this._getRejected()) } } get () { - return this.token + return this.promise + } +} + +export class CancelTokenReference { // implements cancel parts of Action + constructor (cur) { + this.promise = new LiveCancelToken(this) + this.curToken = cur + } + /* istanbul ignore next */ + destroy () { + // possibly called when unsubscribed from curToken + } + cancel (p) { + /* istanbul ignore if */ + if (this.curToken == null || this.curToken.getRejected() !== p) return // when called from an oldToken + // assert: !this.promise._cancelled + return this.promise.__cancel(p) + } + _testRequested () { + return this.curToken != null && this.curToken.requested + } + /* _getRejected () { + return this.curToken.getRejected() + } */ + set (newToken) { + /* eslint complexity:[2,7] */ + const oldToken = this.curToken + if (oldToken && oldToken.requested) { + throw new ReferenceError('token must not be changed after being cancelled') + } + if (oldToken !== newToken && this.promise !== newToken) { + if (oldToken) { + oldToken._unsubscribe(this) + } + this.curToken = newToken + if (newToken) { + if (newToken.requested) { + this.promise.__cancel(newToken.getRejected()) + } else { + newToken._subscribe(this) + } + } + } + } + get () { + return this.promise } } diff --git a/src/Promise.js b/src/Promise.js index e04692c..9a0540d 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -283,7 +283,6 @@ class Rejected extends Core { super() this.value = e this._state = REJECTED // mutated by the silencer - errorHandler.track(this) } then (_, r, token) { @@ -406,6 +405,11 @@ export function silenceError (p) { p._runAction(silencer) } +export function silentReject (e) { + const r = new Rejected(e) + r._state |= HANDLED + return r +} // ------------------------------------------------------------- // ## Creating promises // ------------------------------------------------------------- @@ -432,7 +436,9 @@ export function resolveObject (o) { // reject :: e -> Promise e a export function reject (e) { - return new Rejected(e) + const r = new Rejected(e) + errorHandler.track(r) + return r } // never :: Promise e a diff --git a/src/coroutine.js b/src/coroutine.js index e222189..2af83b8 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -1,4 +1,3 @@ -import { noop } from './util' import { Future, resolve, reject } from './Promise' import CancelToken from './CancelToken' import Action from './Action' @@ -19,36 +18,31 @@ const stack = [] Object.defineProperty(coroutine, 'cancel', { get () { if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') - return stack[stack.length - 1].token + return stack[stack.length - 1].getToken() }, set (token) { if (!stack.length) throw new SyntaxError('coroutine.cancel is only available inside a coroutine') token = CancelToken.from(token) - stack[stack.length - 1].token._follow(token) + stack[stack.length - 1].setToken(token) }, configurable: true }) function runGenerator (generator) { - const promise = new Future(new SwappableCancelToken()) - new Coroutine(generator, promise).run() - // taskQueue.add(new Coroutine(generator, promise)) + const swappable = CancelToken.reference(null) + const promise = new Future(swappable.get()) + new Coroutine(generator, promise, swappable).run() + // taskQueue.add(new Coroutine(generator, promise, swappable)) return promise } class Coroutine extends Action { - constructor (generator, promise) { + constructor (generator, promise, ref) { super(promise) // the generator that is driven. After cancellation, reference to cleanup coroutine this.generator = generator - // the CancelToken that can be directed to follow the current token - this.token = promise.token - } - - destroy () { - super.destroy() - this.generator = null - this.token = null + // a CancelTokenReference + this.tokenref = ref } run () { @@ -67,8 +61,10 @@ class Coroutine extends Action { cancel (p) { /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself + // assert: p === this.promise.token.getRejected() this.promise = null - const res = this.initCancel(p) + const res = new Future() + this.generator = new Coroutine(this.generator, res, p.near().value) if (stack.indexOf(this) < 0) { this.resumeCancel() } @@ -77,7 +73,7 @@ class Coroutine extends Action { } step (f, x) { - /* eslint complexity:[2,4] */ + /* eslint complexity:[2,5] */ let result stack.push(this) try { @@ -102,70 +98,23 @@ class Coroutine extends Action { } } - initCancel (p) { - // assert: p === this.promise.token.getRejected() - const promise = new Future() - this.generator = new Coroutine(this.generator, promise, p.value) - return promise - } - resumeCancel () { const cancelRoutine = this.generator this.generator = null - const reason = cancelRoutine.token - cancelRoutine.token = null // not cancellable - // assert: reason === this.token.getRejected().value - this.token = null + const reason = cancelRoutine.tokenref + cancelRoutine.tokenref = null // not cancellable + // assert: reason === this.tokenref.get().getRejected().value + this.tokenref = null cancelRoutine.step(cancelRoutine.generator.return, reason) } -} - -class SwappableCancelToken extends CancelToken { // also implements cancel parts of Action - constructor () { - super(noop) - this.promise = new Future() - this.curToken = null - } - - destroy () { - // possibly called when unsubscribed from curToken - } - cancel (p) { - if (this._cancelled) return - if (p !== this.curToken.getRejected()) return - this._cancelled = true - this.promise._resolve(p) - return this.run() - } - - get requested () { - if (this.curToken == null) return false - const c = this.curToken.requested - if (c && !this._cancelled) { - this.cancel(this.curToken.getRejected()) - } - return c + setToken (t) { + if (this.tokenref == null) throw new SyntaxError('coroutine.cancel is only available until cancellation') + this.tokenref.set(t) } - _follow (newToken) { - /* eslint complexity:[2,7] */ - const oldToken = this.curToken - if (oldToken && oldToken.requested) { - throw new ReferenceError('token must not be changed after being cancelled') - } - if (oldToken !== newToken && this !== newToken) { - if (oldToken) { - oldToken._unsubscribe(this) - } - this.curToken = newToken - if (newToken) { - if (newToken.requested) { - this.cancel(newToken.getRejected()) - } else { - newToken._subscribe(this) - } - } - } + getToken () { + if (this.tokenref == null) throw new SyntaxError('coroutine.cancel is only available until cancellation') + return this.tokenref.get() } } diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index ee4b780..0735429 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' import { CancelToken, isRejected, isPending, getReason, future, reject } from '../src/main' -import { FakeCancelAction, raceCallbacks } from './lib/test-util' +import { assertSame, FakeCancelAction, raceCallbacks } from './lib/test-util' import assert from 'assert' describe('CancelToken', function () { @@ -428,5 +428,85 @@ describe('CancelToken', function () { assert(pool.get().requested) }) }) + + it('should be requested faster than the subscription', () => { + const {token, cancel} = CancelToken.source() + token.subscribe(() => { + assert(token.requested) + assert(pool.get().requested) + }) + const pool = CancelToken.pool([token]) + return cancel()[0] + }) + + it('should ignore itself', () => { + const {token, cancel} = CancelToken.source() + const pool = CancelToken.pool() + pool.add(pool.get()) + assert.strictEqual(pool.tokens.length, 0) + assert(!pool.get().requested) + pool.add(token) + assert(!pool.get().requested) + cancel() + assert(pool.get().requested) + }) + + it('should reject if already-cancelled tokens are added', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + cancel(expected) + assert(CancelToken.pool([token]).get().requested) + const pool = CancelToken.pool() + pool.add(token) + return pool.get().getRejected().then(assert.ifError, r => { + assert.strictEqual(r.length, 1) + assert.strictEqual(r[0], expected) + }) + }) + }) + + describe('static reference()', () => { + it('should behave like the last assigned token', () => { + const {token, cancel} = CancelToken.source() + const ref = CancelToken.reference() + assert(!ref.get().requested) + ref.set(CancelToken.empty()) + assert(!ref.get().requested) + ref.set(null) + assert(!ref.get().requested) + ref.set(token) + assert(!ref.get().requested) + cancel({}) + assert(ref.get().requested) + return assertSame(token.getRejected(), ref.get().getRejected()) + }) + + it('should throw when assigned to after cancellation', () => { + const {token, cancel} = CancelToken.source() + cancel() + const ref = CancelToken.reference(token) + assert(ref.get().requested) + assert.throws(() => { ref.set(null) }, ReferenceError) + assert.throws(() => { ref.set(CancelToken.empty()) }, ReferenceError) + }) + + it('should be requested faster than the subscription', () => { + const {token, cancel} = CancelToken.source() + token.subscribe(() => { + assert(token.requested) + assert(ref.get().requested) + }) + const ref = CancelToken.reference(token) + return cancel()[0] + }) + + it('should ignore itself', () => { + const {token, cancel} = CancelToken.source() + const ref = CancelToken.reference(token) + ref.set(token) + ref.set(ref.get()) + cancel() + assert(ref.get().requested) + }) }) }) diff --git a/test/coroutine-test.js b/test/coroutine-test.js index 529d92e..63e129e 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -180,27 +180,6 @@ describe('coroutine', function () { }) describe('coroutine.cancel', () => { - it('should behave like the last assigned token', () => { - const {token, cancel} = CancelToken.source() - let c_token - const p = coroutine(function* () { - assert(!coroutine.cancel.requested) - coroutine.cancel = CancelToken.empty() - assert(!coroutine.cancel.requested) - coroutine.cancel = null - assert(!coroutine.cancel.requested) - coroutine.cancel = token - assert(!coroutine.cancel.requested) - cancel({}) - assert(coroutine.cancel.requested) - c_token = coroutine.cancel - })() - return p.then(assert.ifError, e => { - assert.strictEqual(c_token, p.token) - return assertSame(token.getRejected(), c_token.getRejected()) - }) - }) - it('should always return the same token', () => { return coroutine(function* () { const token = coroutine.cancel @@ -214,11 +193,41 @@ describe('coroutine', function () { })() }) - it('should throw when used outside a coroutine', () => { + it('should return the token of the result promise', () => { + const p = coroutine(function* () { + return coroutine.cancel + })() + return p.then(token => { + assert.strictEqual(token, p.token) + }) + }) + + it('should not be available outside a coroutine', () => { assert.throws(() => coroutine.cancel, SyntaxError) assert.throws(() => { coroutine.cancel = null }, SyntaxError) }) + it('should not be available in finally blocks after cancellation', () => { + let err + const p = coroutine(function* () { + const {token, cancel} = CancelToken.source() + coroutine.cancel = token + try { + yield delay(1) + yield cancel() + } finally { + try { + assert(isRejected(p)) + assert.throws(() => coroutine.cancel, SyntaxError) + assert.throws(() => { coroutine.cancel = null }, SyntaxError) + } catch (e) { + err = e + } + } + })() + return p.then(assert.ifError, () => assert.ifError(err)) + }) + it('should throw when assigned to after cancellation', () => { let err const p = coroutine(function* () { From c945325c657589714523b39e031b53c566ec4890 Mon Sep 17 00:00:00 2001 From: Bergi Date: Tue, 5 Jul 2016 23:21:17 +0200 Subject: [PATCH 19/28] sanity improvements for CancelToken::concat * refactored common elements of CancelTokenPool and CancelTokenReference into CancelTokenCombinator * added CancelToken.race() with CancelTokenRace * tested them --- src/CancelToken.js | 112 +++++++++++++++++++++++++-------------- test/CancelToken-test.js | 42 +++++++++++++++ 2 files changed, 115 insertions(+), 39 deletions(-) diff --git a/src/CancelToken.js b/src/CancelToken.js index cc1765e..5fd9796 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -136,12 +136,10 @@ export default class CancelToken { return new this(noop) // NeverCancelToken } concat (token) { - if (this.requested) return this - if (token.requested) return token - return new CancelToken(cancel => { - this.subscribe(cancel) - token.subscribe(cancel) - }) + return new CancelTokenRace([this, token]).get() + } + static race (tokens) { + return new CancelTokenRace(tokens) } static pool (tokens) { return new CancelTokenPool(tokens) @@ -162,36 +160,85 @@ class LiveCancelToken extends CancelToken { } get requested () { return this._cancelled || this.check._testRequested() - /* if (this._cancelled) return true - const c = this.check._testRequested() - if (c) { - this.__cancel(this.check._getRejected()) + } +} + +class CancelTokenCombinator { // implements cancel parts of Action + constructor () { + // should be named "token" but is necessary for Action-like usage + this.promise = new LiveCancelToken(this) + } + /* istanbul ignore next */ + destroy () { + // possibly called when unsubscribed from a token + } + // abstract cancel (p) {} + // abstract _testRequested () {} + get () { + return this.promise + } +} + +class CancelTokenRace extends CancelTokenCombinator { + constructor (tokens) { + super() + this.tokens = [] + if (tokens) this.add(...tokens) + } + cancel (p) { + /* istanbul ignore if */ + if (this.tokens == null) return // when called after been unsubscribed but not destroyed + // assert: !this.promise._cancelled + // for (let t of this.tokens) { // https://phabricator.babeljs.io/T2164 + for (let i = 0, t; i < this.tokens.length && (t = this.tokens[i]); i++) { + t._unsubscribe(this) + } + this.tokens = null + return this.promise.__cancel(p) + } + _testRequested () { + return this.tokens.some(t => t.requested) + } + add (...tokens) { + if (this.tokens == null) return + // for (let t of tokens) { // https://phabricator.babeljs.io/T2164 + for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) { + t = CancelToken.from(t) + if (t === this.promise || t == null) { + continue + } + if (t.requested) { + this.cancel(t.getRejected()) + break + } else { + this.tokens.push(t) + t._subscribe(this) + } } - return c */ } } -class CancelTokenPool { // implements cancel parts of Action +class CancelTokenPool extends CancelTokenCombinator { constructor (tokens) { - this.promise = new LiveCancelToken(this) + super() this.tokens = [] this.count = 0 if (tokens) this.add(...tokens) } - // never called (by unsubscribe): destroy () {} cancel (p) { // assert: !this.promise._cancelled - if (--this.count === 0) { - return this.promise.__cancel(this._getRejected()) - } + this.count-- + return this._check() } _testRequested () { return this.tokens.length > 0 && this.tokens.every(t => t.requested) } - _getRejected () { - const reasons = this.tokens.map(t => t.getRejected().near().value) - this.tokens = null - return silentReject(reasons) + _check () { + if (this.count === 0) { + const reasons = this.tokens.map(t => t.getRejected().near().value) + this.tokens = null + return this.promise.__cancel(silentReject(reasons)) + } } add (...tokens) { if (this.tokens == null) return @@ -199,7 +246,7 @@ class CancelTokenPool { // implements cancel parts of Action // for (let t of tokens) { // https://phabricator.babeljs.io/T2164 for (let i = 0, t; i < tokens.length && (t = tokens[i]); i++) { t = CancelToken.from(t) - if (this.promise === t) { + if (t === this.promise || t == null) { this.count-- continue } @@ -210,24 +257,17 @@ class CancelTokenPool { // implements cancel parts of Action t._subscribe(this) } } - if (this.tokens.length > 0 && this.count === 0) { - this.promise.__cancel(this._getRejected()) + if (this.tokens.length > 0) { + this._check() } } - get () { - return this.promise - } } -export class CancelTokenReference { // implements cancel parts of Action +export class CancelTokenReference extends CancelTokenCombinator { constructor (cur) { - this.promise = new LiveCancelToken(this) + super() this.curToken = cur } - /* istanbul ignore next */ - destroy () { - // possibly called when unsubscribed from curToken - } cancel (p) { /* istanbul ignore if */ if (this.curToken == null || this.curToken.getRejected() !== p) return // when called from an oldToken @@ -237,9 +277,6 @@ export class CancelTokenReference { // implements cancel parts of Action _testRequested () { return this.curToken != null && this.curToken.requested } - /* _getRejected () { - return this.curToken.getRejected() - } */ set (newToken) { /* eslint complexity:[2,7] */ const oldToken = this.curToken @@ -260,7 +297,4 @@ export class CancelTokenReference { // implements cancel parts of Action } } } - get () { - return this.promise - } } diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index 0735429..b557039 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -352,6 +352,48 @@ describe('CancelToken', function () { }) }) + describe('static race()', () => { + // see also concat test cases + it('should ignore tokens added after cancellation', () => { + const race = CancelToken.race() + const a = CancelToken.source() + race.add(a.token) + const expected = {} + a.cancel(expected) + assert(race.get().requested) + const b = CancelToken.source() + b.cancel() + race.add(b.token) + assert(race.get().requested) + return race.get().getRejected().then(assert.ifError, e => { + assert.strictEqual(e, expected) + const c = CancelToken.source() + race.add(c.token) + assert(race.get().requested) + }) + }) + + it('should be requested faster than the subscription', () => { + const {token, cancel} = CancelToken.source() + token.subscribe(() => { + assert(token.requested) + assert(race.get().requested) + }) + const race = CancelToken.race([token]) + return cancel()[0] + }) + + it('should ignore itself', () => { + const {token, cancel} = CancelToken.source() + const race = CancelToken.race() + race.add(race.get()) + assert(!race.get().requested) + race.add(token) + cancel() + assert(race.get().requested) + }) + }) + describe('static pool()', () => { it('should cancel when all tokens are cancelled', () => { const sources = [] From a8a4bf5de00dcfbe36df9c6547210977ea5c96e4 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 6 Jul 2016 04:33:04 +0200 Subject: [PATCH 20/28] rewrite CancelToken::subscribe, add subscribeOrCall * Now behaving more like .getRejected().catch(...) * taking a token instead of a promise * ...OrCall being the super-easy way to cancel a subscription --- src/CancelToken.js | 30 ++++---- src/subscribe.js | 57 +++++++++++++++ test/CancelToken-test.js | 154 +++++++++++++++++++++++++++++++++------ 3 files changed, 203 insertions(+), 38 deletions(-) create mode 100644 src/subscribe.js diff --git a/src/CancelToken.js b/src/CancelToken.js index 5fd9796..88ff984 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,6 +1,6 @@ import { noop } from './util' -import { Future, resolve, reject, silentReject, never, taskQueue } from './Promise' // deferred -import { isSettled } from './inspect' +import { Future, resolve, reject, silentReject, taskQueue } from './Promise' // deferred +import { subscribe, subscribeOrCall } from './subscribe' export default class CancelToken { // https://domenic.github.io/cancelable-promise/#sec-canceltoken-constructor @@ -33,13 +33,18 @@ export default class CancelToken { run () { /* eslint complexity:[2,4] */ const result = [] - for (let i = 0; i < this.length; ++i) { + const l = this.length + for (let i = 0; i < l; ++i) { if (this[i] && this[i].promise) { // not already destroyed this._runAction(this[i], result) } this[i] = void 0 } - this.length = 0 + if (this.length === l) { + this.length = 0 + } else { + taskQueue.add(this) + } return result } _runAction (action, results) { @@ -86,18 +91,11 @@ export default class CancelToken { action.destroy() // at least mark explictly as empty } } - subscribe (fn, promise) { - promise = promise != null ? resolve(promise) : never() - this._subscribe({ - promise, - cancel (p) { - if (!isSettled(this.promise)) { - return resolve(fn(p.near().value)) - } - } - }) - // TODO unsubscribe when promise settles - return this + subscribe (fn, token) { + return subscribe(fn, this, new Future(token)) + } + subscribeOrCall (fn, c) { + return subscribeOrCall(fn, c, this, new Future()) } getRejected () { if (this.promise === void 0) { diff --git a/src/subscribe.js b/src/subscribe.js new file mode 100644 index 0000000..4126ffb --- /dev/null +++ b/src/subscribe.js @@ -0,0 +1,57 @@ +import Action from './Action' + +export function subscribe (f, t, promise) { + if (promise.token != null && promise.token.requested) { + return promise.token.getRejected() + } + t._subscribe(new Subscription(f, promise)) + return promise +} + +export function subscribeOrCall (f, g, t, promise) { + let sub = new Subscription(f, promise) + t._subscribe(sub) + return function call () { + // TODO: should `g` run despite `t.requested`, + // or should none run immediately despite `call` having been called? + if (sub != null && sub.f != null) { + t._unsubscribe(sub) + t = sub = null + if (typeof g === 'function') { + return g.apply(this, arguments) + } + } + } +} + +class Subscription extends Action { + constructor (f, promise) { + super(promise) + this.f = f + } + + destroy () { + super.destroy() + this.f = null + } + + cancel (p) { + /* eslint complexity:[2,4] */ + const token = this.promise.token + const f = this.f + if (token != null && this.promise._isResolved()) { // promise checks for cancellation itself + if (f != null) { // avoid destruction in case of reentrancy + this.destroy() + } + } else { + this.f = null + this.tryCall(f, p.near().value) + if (token != null) token._unsubscribe(this) + return this.promise + } + } + + handle (result) { + this.promise._resolve(result) + } +} diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index b557039..3ddd6aa 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -248,38 +248,148 @@ describe('CancelToken', function () { assert.strictEqual(s, 2) }) - it('should call subscriptions when the promise is not settled', () => { + it('should return a promise for the result', () => { const {token, cancel} = CancelToken.source() - const {promise} = future() - const {ok, result} = raceCallbacks(future) - token.subscribe(ok, promise) + const expected = {} + const p = token.subscribe(() => expected) + assert.strictEqual(cancel()[0], p) + return p.then(x => { + assert.strictEqual(x, expected) + }) + }) + + it('should behave nearly like getRejected().catch()', () => { + const {token, cancel} = CancelToken.source() + const p = token.subscribe(x => x) + const q = token.getRejected().catch(x => x) + const res = cancel({}) + assert.strictEqual(res.length, 1) + assert.strictEqual(res[0], p) + return assertSame(p, q) + }) + + it('should behave like rejection for throw', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + const p = token.subscribe(() => { throw expected }) + assert.strictEqual(cancel()[0], p) + return p.then(assert.ifError, x => { + assert.strictEqual(x, expected) + }) + }) + + it('should call subscriptions before the token is cancelled', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + const p = token.subscribe(() => expected, CancelToken.empty()) cancel() - return result + return p.then(x => { + assert.strictEqual(x, expected) + }) + }) + + it('should not call subscriptions when the token is cancelled', () => { + const a = CancelToken.source() + const b = CancelToken.source() + let called = false + const p = a.token.subscribe(() => { + called = true + }, b.token) + assert.strictEqual(p.token, b.token) + b.cancel() + assert(!called) + a.cancel() + assert(!called) + return assertSame(p, b.token.getRejected()) }) - it('should not call subscriptions when the promise is fulfilled', () => { + it('should return cancellation with already cancelled token', () => { const {token, cancel} = CancelToken.source() - const {resolve, promise} = future() - const {ok, nok, result} = raceCallbacks(future) - token.subscribe(nok, promise) - promise.then(cancel).then(ok) - resolve() - return result + cancel() + const p = CancelToken.empty().subscribe(() => {}, token) + assert.strictEqual(p, token.getRejected()) + }) + + it('should behave like cancellation when token is cancelled from the subscription', () => { + const a = CancelToken.source() + const b = CancelToken.source() + const p = a.token.subscribe(() => { + b.cancel() + }, b.token) + assert.strictEqual(p.token, b.token) + a.cancel() + return assertSame(p, b.token.getRejected()) }) - it('should not call subscriptions when the promise is rejected', () => { + it('should call subscriptions that are subscribed from the callback', () => { const {token, cancel} = CancelToken.source() - const {resolve, promise} = future() - const {ok, nok, result} = raceCallbacks(future) - token.subscribe(nok, promise) - promise.catch(cancel).then(ok) - resolve(reject()) - return result + const expected = {} + const p = token.subscribe(() => token.subscribe(() => expected)) + assert.strictEqual(cancel().length, 1) + return p.then(x => { + assert.strictEqual(x, expected) + }) + }) + }) + + describe('subscribeOrCall()', () => { + it('should invoke f if the token is cancelled before the call', () => { + const {token, cancel} = CancelToken.source() + let called = 0 + const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2 }) + assert.strictEqual(called, 0) + cancel() + assert.strictEqual(called, 1) + call() + assert.strictEqual(called, 1) + }) + + it('should invoke g if the call happens before the cancellation', () => { + const {token, cancel} = CancelToken.source() + let called = 0 + const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2 }) + assert.strictEqual(called, 0) + call() + assert.strictEqual(called, 2) + cancel() + assert.strictEqual(called, 2) + }) + + it('should cope with undefined g', () => { + const {token, cancel} = CancelToken.source() + let called = 0 + const call = token.subscribeOrCall(() => { called = 0 }) + assert.strictEqual(called, 0) + call() + assert.strictEqual(called, 0) + cancel() + assert.strictEqual(called, 0) + }) + + it('should throw exceptions from g', () => { + const expected = {} + const call = CancelToken.empty().subscribeOrCall(() => {}, () => { throw expected }) + assert.throws(call, e => e === expected) + }) + + it('should invoke g with the arguments and context of the call', () => { + const o = {a: {}, b: []} + const call = CancelToken.empty().subscribeOrCall(() => {}, function(a, b) { + assert.strictEqual(this, o) + assert.strictEqual(a, o.a) + assert.strictEqual(b, o.b) + }) + call.call(o, o.a, o.b) }) - it('should return the token', () => { - const {token} = CancelToken.source() - assert.strictEqual(token.subscribe(() => {}), token) + it('should not invoke g multiple times', () => { + let called = 0 + const call = CancelToken.empty().subscribeOrCall(() => {}, () => { called++ }) + assert.strictEqual(called, 0) + call() + assert.strictEqual(called, 1) + call() + assert.strictEqual(called, 1) }) }) From 5500cf6ac37f564dcb07d55107db4d355e44bf90 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 6 Jul 2016 06:13:46 +0200 Subject: [PATCH 21/28] simplify unsubscribing Actions --- src/Action.js | 40 +++++++++++++++++++++++++--------------- src/chain.js | 2 -- src/coroutine.js | 7 +++---- src/delay.js | 8 +++----- src/map.js | 2 -- src/subscribe.js | 7 +++---- src/then.js | 4 +--- 7 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/Action.js b/src/Action.js index 56afe23..bcd6839 100644 --- a/src/Action.js +++ b/src/Action.js @@ -23,28 +23,38 @@ export default class Action { // default onFulfilled action /* istanbul ignore next */ fulfilled (p) { - const token = this.promise.token - this.promise._become(p) - if (token != null) token._unsubscribe(this) + this.put(p) } // default onRejected action rejected (p) { - const token = this.promise.token - this.promise._become(p) - if (token != null) token._unsubscribe(this) + this.put(p) return false } tryCall (f, x) { - /* eslint complexity:[2,4] */ - let result - try { - result = f(x) - } catch (e) { - if (this.promise) this.promise._reject(e) - return - } // else - if (this.promise) this.handle(result) + /* eslint complexity:[2,5], no-labels:0, no-lone-blocks:0 */ + call: { + let result + try { + result = f(x) + } catch (e) { + if (this.promise == null) return // got cancelled during call + this.promise._reject(e) + break call + } /* else */ { + if (this.promise == null) return // got cancelled during call + this.handle(result) + } + } + const token = this.promise.token + if (token != null) token._unsubscribe(this) + } + + put (p) { + const promise = this.promise + const token = promise.token + promise._become(p) + if (token != null) token._unsubscribe(this) } } diff --git a/src/chain.js b/src/chain.js index 73ed43a..da2ecc4 100644 --- a/src/chain.js +++ b/src/chain.js @@ -21,9 +21,7 @@ class Chain extends Action { } fulfilled (p) { - const token = this.promise.token this.tryCall(this.f, p.value) - if (token != null) token._unsubscribe(this) } handle (y) { diff --git a/src/coroutine.js b/src/coroutine.js index 2af83b8..e3a2aeb 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -84,12 +84,11 @@ class Coroutine extends Action { stack.pop() // assert: === this } if (this.promise) { - const token = this.promise.token + const res = resolve(result.value, this.promise.token) if (result.done) { - this.promise._resolve(result.value) - if (token != null) token._unsubscribe(this) + this.put(res) } else { - resolve(result.value, token)._runAction(this) + res._runAction(this) } } else { // cancelled during execution // ignoring result.done and result.value diff --git a/src/delay.js b/src/delay.js index 2341898..8e55d8f 100644 --- a/src/delay.js +++ b/src/delay.js @@ -24,12 +24,10 @@ class Delay extends Action { fulfilled (p) { /* global setTimeout */ - this.id = setTimeout(become, this.time, p, this) + this.id = setTimeout(put, this.time, p, this) } } -function become (p, action) { - const token = action.promise.token - action.promise._become(p) - if (token != null) token._unsubscribe(action) +function put (p, action) { + action.put(p) } diff --git a/src/map.js b/src/map.js index b56a1fe..cbd7cbb 100644 --- a/src/map.js +++ b/src/map.js @@ -20,9 +20,7 @@ class Map extends Action { } fulfilled (p) { - const token = this.promise.token this.tryCall(this.f, p.value) - if (token != null) token._unsubscribe(this) } handle (result) { diff --git a/src/subscribe.js b/src/subscribe.js index 4126ffb..6d5c95f 100644 --- a/src/subscribe.js +++ b/src/subscribe.js @@ -37,17 +37,16 @@ class Subscription extends Action { cancel (p) { /* eslint complexity:[2,4] */ - const token = this.promise.token + const promise = this.promise const f = this.f - if (token != null && this.promise._isResolved()) { // promise checks for cancellation itself + if (promise.token != null && promise._isResolved()) { // promise checks for cancellation itself if (f != null) { // avoid destruction in case of reentrancy this.destroy() } } else { this.f = null this.tryCall(f, p.near().value) - if (token != null) token._unsubscribe(this) - return this.promise + return promise } } diff --git a/src/then.js b/src/then.js index 47db529..e51f59f 100644 --- a/src/then.js +++ b/src/then.js @@ -30,14 +30,12 @@ class Then extends Action { } runThen (f, p) { - const token = this.promise.token const hasHandler = typeof f === 'function' if (hasHandler) { this.tryCall(f, p.value) } else { - this.promise._become(p) + this.put(p) } - if (token != null) token._unsubscribe(this) return hasHandler } From 8663d25ebaccdebb41be4772d3dc751dbac8aa66 Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 6 Jul 2016 21:51:34 +0200 Subject: [PATCH 22/28] add Promise::finally * rewrite Action::cancel, Action::tryCall * fix some tests * add tests for finally --- src/Action.js | 44 ++++++---- src/CancelToken.js | 24 +++--- src/Promise.js | 18 ++++ src/chain.js | 2 +- src/finally.js | 85 +++++++++++++++++++ src/map.js | 2 +- src/subscribe.js | 16 ++-- src/then.js | 2 +- test/CancelToken-test.js | 34 +++++--- test/coroutine-test.js | 2 +- test/finally-test.js | 173 +++++++++++++++++++++++++++++++++++++++ test/never-test.js | 5 ++ 12 files changed, 354 insertions(+), 53 deletions(-) create mode 100644 src/finally.js create mode 100644 test/finally-test.js diff --git a/src/Action.js b/src/Action.js index bcd6839..9e61eef 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,3 +1,8 @@ +import { Future } from './Promise' + +let sentinel = null +const empty = [] + export default class Action { constructor (promise) { this.promise = promise // the Future which this Action tries to resolve @@ -14,9 +19,15 @@ export default class Action { } cancel (p) { - /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself - this.destroy() + if (this.promise === sentinel) { + this.destroy() + this.promise = new Future() + return this.promise + } else { + this.destroy() + return empty + } } } @@ -33,22 +44,24 @@ export default class Action { } tryCall (f, x) { - /* eslint complexity:[2,5], no-labels:0, no-lone-blocks:0 */ - call: { - let result - try { - result = f(x) - } catch (e) { - if (this.promise == null) return // got cancelled during call - this.promise._reject(e) - break call - } /* else */ { - if (this.promise == null) return // got cancelled during call - this.handle(result) - } + const original = sentinel = this.promise + let result + try { + result = f(x) + } catch (e) { + sentinel = null + this.promise._reject(e) + return this.promise === original } + sentinel = null + this.handle(result) + return this.promise === original + } + + tryUnsubscribe () { const token = this.promise.token if (token != null) token._unsubscribe(this) + this.promise = null } put (p) { @@ -56,5 +69,6 @@ export default class Action { const token = promise.token promise._become(p) if (token != null) token._unsubscribe(this) + this.promise = null } } diff --git a/src/CancelToken.js b/src/CancelToken.js index 88ff984..572b23f 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,5 +1,5 @@ import { noop } from './util' -import { Future, resolve, reject, silentReject, taskQueue } from './Promise' // deferred +import { Future, resolve, silentReject, taskQueue } from './Promise' // deferred import { subscribe, subscribeOrCall } from './subscribe' export default class CancelToken { @@ -24,7 +24,7 @@ export default class CancelToken { __cancel (p) { this._cancelled = true if (this.promise !== void 0) { - this.promise._resolve(p) + this.promise.__become(p) } else { this.promise = p } @@ -48,22 +48,20 @@ export default class CancelToken { return result } _runAction (action, results) { - try { - const res = action.cancel(this.promise) - if (res != null) { - if (Array.isArray(res)) { + const res = action.cancel(this.promise) + if (res != null) { + if (Array.isArray(res)) { + if (res.length > 0) { results.push(...res) - } else { - results.push(res) } + } else { + results.push(res) } - } catch (e) { - results.push(reject(e)) } } _subscribe (action) { if (this.requested && this.length === 0) { - taskQueue.add(this) // asynchronous? + taskQueue.add(this) } this[this.length++] = action } @@ -99,9 +97,9 @@ export default class CancelToken { } getRejected () { if (this.promise === void 0) { - this.promise = new Future() // never cancelled :-) + this.promise = new Future(this) // while not settled, provides a reference to token } - return this.promise + return this.promise.near() // TODO: always return same instance? } // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested get requested () { diff --git a/src/Promise.js b/src/Promise.js index 9a0540d..8eaf4a8 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -10,6 +10,7 @@ import Action from './Action' import then from './then' import map from './map' import chain from './chain' +import fin from './finally' import CancelToken from './CancelToken' @@ -117,6 +118,11 @@ export class Future extends Core { return p } + finally (f) { + const n = this.near() + return n === this ? fin(f, this, new Future()) : n.finally(f) + } + // toString :: Promise e a -> String toString () { return '[object ' + this.inspect() + ']' @@ -250,6 +256,10 @@ class Fulfilled extends Core { return rejectedIfCancelled(token, this) } + finally (f) { + return fin(f, this, new Future()) + } + toString () { return '[object ' + this.inspect() + ']' } @@ -313,6 +323,10 @@ class Rejected extends Core { return rejectedIfCancelled(token, this) } + finally (f) { + return fin(f, this, new Future()) + } + toString () { return '[object ' + this.inspect() + ']' } @@ -372,6 +386,10 @@ class Never extends Core { return rejectedWhenCancel(token, this) } + finally (_) { + return this + } + toString () { return '[object ' + this.inspect() + ']' } diff --git a/src/chain.js b/src/chain.js index da2ecc4..21ded18 100644 --- a/src/chain.js +++ b/src/chain.js @@ -21,7 +21,7 @@ class Chain extends Action { } fulfilled (p) { - this.tryCall(this.f, p.value) + if (this.tryCall(this.f, p.value)) this.tryUnsubscribe() } handle (y) { diff --git a/src/finally.js b/src/finally.js new file mode 100644 index 0000000..2aef39d --- /dev/null +++ b/src/finally.js @@ -0,0 +1,85 @@ +import { resolve } from './Promise' +import { isRejected, isFulfilled } from './inspect' +import Action from './Action' + +export default function _finally (f, p, promise) { + // assert: promise.token == null + if (typeof f !== 'function') throw new TypeError('finally does require a callback function') + p._when(new Final(f, p.token, promise)) + return promise +} + +class Final extends Action { + constructor (f, t, promise) { + super(promise) + this.token = t + if (t != null) { + t._subscribe(this) + } + this.f = f + } + + /* istanbul ignore next */ + destroy () { // possibly called when unsubscribed from the token + this.token = null + } + + cancel (p) { + /* istanbul ignore if */ + if (this.token == null) return + this.token = null + return this.tryFin(p) + } + + fulfilled (p) { + this.tryFin(p) + } + + rejected (p) { + this.tryFin(p) + return true // TODO: correctness? track again afterwards? + } + + tryFin (p) { + /* eslint complexity:[2,5] */ + const f = this.f + if (typeof f !== 'function') return this.promise + this.f = null + const token = this.token + if (token) { + token._unsubscribe(this) + this.token = null + } + const orig = this.promise + if (!this.tryCall(f, p)) { + // assert: orig !== this.promise + // assert: !isRejeced(this.promise) + if (isFulfilled(this.promise)) { + orig._become(p) + } else { + this.promise._runAction(new Put(p, orig)) + } + } + return this.promise + } + + handle (result) { + const p = resolve(result) + if (isRejected(p)) { + this.promise._become(p) + } else { + this.promise = p + } + } +} + +class Put extends Action { + constructor (promise, target) { + super(target) + this.p = promise + } + + fulfilled (_) { + this.put(this.p) + } +} diff --git a/src/map.js b/src/map.js index cbd7cbb..4038380 100644 --- a/src/map.js +++ b/src/map.js @@ -20,7 +20,7 @@ class Map extends Action { } fulfilled (p) { - this.tryCall(this.f, p.value) + if (this.tryCall(this.f, p.value)) this.tryUnsubscribe() } handle (result) { diff --git a/src/subscribe.js b/src/subscribe.js index 6d5c95f..41c501d 100644 --- a/src/subscribe.js +++ b/src/subscribe.js @@ -38,16 +38,16 @@ class Subscription extends Action { cancel (p) { /* eslint complexity:[2,4] */ const promise = this.promise - const f = this.f - if (promise.token != null && promise._isResolved()) { // promise checks for cancellation itself - if (f != null) { // avoid destruction in case of reentrancy - this.destroy() + if (promise.token != null) { + const res = super.cancel(p) + if (res != null) { + return res } - } else { - this.f = null - this.tryCall(f, p.near().value) - return promise } + const f = this.f + this.f = null + if (this.tryCall(f, p.near().value)) this.tryUnsubscribe() + return promise } handle (result) { diff --git a/src/then.js b/src/then.js index e51f59f..6d8e15c 100644 --- a/src/then.js +++ b/src/then.js @@ -32,7 +32,7 @@ class Then extends Action { runThen (f, p) { const hasHandler = typeof f === 'function' if (hasHandler) { - this.tryCall(f, p.value) + if (this.tryCall(f, p.value)) this.tryUnsubscribe() } else { this.put(p) } diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index 3ddd6aa..d73f0d4 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { CancelToken, isRejected, isPending, getReason, future, reject } from '../src/main' -import { assertSame, FakeCancelAction, raceCallbacks } from './lib/test-util' +import { CancelToken, isRejected, isPending, getReason, future } from '../src/main' +import { assertSame, FakeCancelAction } from './lib/test-util' import assert from 'assert' describe('CancelToken', function () { @@ -169,16 +169,6 @@ describe('CancelToken', function () { for (const action of inactive) assert(!action.isCancelled) }) - it('should ignore exceptions thrown by subscriptions', () => { - const {token, cancel} = CancelToken.source() - const throwAction = new FakeCancelAction({}, () => { throw new Error() }) - token._subscribe(throwAction) - const action = new FakeCancelAction({}) - token._subscribe(action) - cancel() - assert(action.isCancelled) - }) - it('should run subscriptions when already requested', () => { const {token, cancel} = CancelToken.source() const {resolve, promise} = future() @@ -355,6 +345,24 @@ describe('CancelToken', function () { assert.strictEqual(called, 2) }) + it('should invoke f if the token is cancelled during the call', () => { + const {token, cancel} = CancelToken.source() + let called = 0 + const call = token.subscribeOrCall(() => { called |= 1; call() }, () => { called |= 2 }) + assert.strictEqual(called, 0) + cancel() + assert.strictEqual(called, 1) + }) + + it('should invoke g if the call happens during the cancellation', () => { + const {token, cancel} = CancelToken.source() + let called = 0 + const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2; cancel() }) + assert.strictEqual(called, 0) + call() + assert.strictEqual(called, 2) + }) + it('should cope with undefined g', () => { const {token, cancel} = CancelToken.source() let called = 0 @@ -374,7 +382,7 @@ describe('CancelToken', function () { it('should invoke g with the arguments and context of the call', () => { const o = {a: {}, b: []} - const call = CancelToken.empty().subscribeOrCall(() => {}, function(a, b) { + const call = CancelToken.empty().subscribeOrCall(() => {}, function (a, b) { assert.strictEqual(this, o) assert.strictEqual(a, o.a) assert.strictEqual(b, o.b) diff --git a/test/coroutine-test.js b/test/coroutine-test.js index 63e129e..9e310f8 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -174,7 +174,7 @@ describe('coroutine', function () { return delay(15).then(() => { cancel({}) assert(isRejected(p)) - assert(counter == 0) + assert.strictEqual(counter, 0) }) }) }) diff --git a/test/finally-test.js b/test/finally-test.js new file mode 100644 index 0000000..c2c0992 --- /dev/null +++ b/test/finally-test.js @@ -0,0 +1,173 @@ +import { describe, it } from 'mocha' +import { future, fulfill, reject, delay, CancelToken } from '../src/main' +import { assertSame } from './lib/test-util' +import assert from 'assert' + +describe('finally', () => { + it('should throw when f is not a function', () => { + const p = fulfill() + assert.throws(() => p.finally(), TypeError) + assert.throws(() => p.finally(''), TypeError) + assert.throws(() => p.finally(1), TypeError) + assert.throws(() => p.finally(false), TypeError) + }) + + it('should call f for fulfill', () => { + let called = false + return fulfill().finally(() => { + called = true + }).then(() => { + assert(called) + }) + }) + + it('should call f for reject', () => { + let called = false + return reject().finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + }) + + it('should call f for future fulfill', () => { + let called = false + const {promise, resolve} = future() + const p = promise.finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + resolve(fulfill()) + return p + }) + + it('should call f for future reject', () => { + let called = false + const {promise, resolve} = future() + const p = promise.finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + resolve(reject()) + return p + }) + + it('should call f with uncancelled token for future fulfill', () => { + let called = false + const {promise, resolve} = future(CancelToken.empty()) + const p = promise.finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + resolve(fulfill()) + return p + }) + + it('should call f with uncancelled token for already fulfilled future', () => { + let called = false + const {promise, resolve} = future(CancelToken.empty()) + resolve(fulfill()) + return promise.finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + }) + + describe('cancel', () => { + it('should call f synchronously', () => { + let called = false + const {token, cancel} = CancelToken.source() + const p = delay(1, null, token).finally(() => { + called = true + }).then(assert.ifError, () => { + assert(called) + }) + assert(!called) + cancel() + assert(called) + return p + }) + + it('should return fulfilled callback result', () => { + const expected = fulfill({}) + const reason = new Error('cancelled') + const {token, cancel} = CancelToken.source() + const p = delay(1, null, token).finally(() => { + return expected + }).then(assert.ifError, e => { + assert.strictEqual(e, reason) + return assertSame(c[0], expected) + }) + const c = cancel(reason) + return p + }) + + it('should return callback exception', () => { + const expected = {} + const {token, cancel} = CancelToken.source() + const p = delay(1, null, token).finally(() => { + throw expected + }).then(assert.ifError, e => { + assert.strictEqual(e, expected) + return assertSame(c[0], reject(expected)) + }) + const c = cancel() + return p + }) + + it('should do nothing during f call for fulfilled future', () => { + const expected = {} + const {token, cancel} = CancelToken.source() + let c + return delay(1, fulfill(expected), token).finally(() => { + c = cancel({}) + }).then(x => { + assert.strictEqual(x, expected) + assert.strictEqual(c.length, 0) + }) + }) + }) + + describe('return value', () => { + it('should behave like input for fulfill', () => { + const p = fulfill({}) + return assertSame(p.finally(() => {}), p) + }) + + it('should behave like input for reject', () => { + const p = reject({}) + return assertSame(p.finally(() => {}), p) + }) + + it('should not resolve before the callback result', () => { + let called = false + const expected = {} + return fulfill(expected).finally(() => { + return delay(3).then(() => { called = true }) + }).then(x => { + assert.strictEqual(x, expected) + assert(called) + }) + }) + + it('should behave like rejection for throwing callback', () => { + const expected = {} + return fulfill().finally(() => { + throw expected + }).then(assert.ifError, e => { + assert.strictEqual(e, expected) + }) + }) + + it('should behave like rejection for rejecting callback', () => { + const p = reject({}) + return assertSame(p, fulfill().finally(() => { + return p + })) + }) + }) +}) diff --git a/test/never-test.js b/test/never-test.js index 1db5b0a..9cdcbd9 100644 --- a/test/never-test.js +++ b/test/never-test.js @@ -58,6 +58,11 @@ describe('never', () => { assert.strictEqual(token.getRejected(), p.chain(assert.ifError, token)) }) + it('finally should be identity', () => { + const p = never() + assert.strictEqual(p, p.finally(() => {})) + }) + it('_when should not call action', () => { let fail = () => { throw new Error('never._when called action') } let action = { From f34e8585e48dffdcb167dd10faf01b1db0a42a27 Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 7 Jul 2016 05:20:56 +0200 Subject: [PATCH 23/28] add Promise::trifurcate * with lots of tests * also add tests and fixes for .untilCancel() --- src/CancelToken.js | 3 +- src/Promise.js | 18 ++ src/trifurcate.js | 59 ++++++ test/fulfill-test.js | 5 + test/never-test.js | 5 + test/reject-test.js | 6 + test/trifurcate-test.js | 444 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 src/trifurcate.js create mode 100644 test/trifurcate-test.js diff --git a/src/CancelToken.js b/src/CancelToken.js index 572b23f..9a3b20e 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -26,6 +26,7 @@ export default class CancelToken { if (this.promise !== void 0) { this.promise.__become(p) } else { + p.token = this // TODO ugly but necessary? this.promise = p } return this.run() @@ -99,7 +100,7 @@ export default class CancelToken { if (this.promise === void 0) { this.promise = new Future(this) // while not settled, provides a reference to token } - return this.promise.near() // TODO: always return same instance? + return this.promise } // https://domenic.github.io/cancelable-promise/#sec-canceltoken.prototype.requested get requested () { diff --git a/src/Promise.js b/src/Promise.js index 8eaf4a8..59e85e8 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -11,6 +11,7 @@ import then from './then' import map from './map' import chain from './chain' import fin from './finally' +import trifurcate from './trifurcate' import CancelToken from './CancelToken' @@ -123,6 +124,11 @@ export class Future extends Core { return n === this ? fin(f, this, new Future()) : n.finally(f) } + trifurcate (f, r, c) { + const n = this.near() + return n === this ? this.token ? trifurcate(f, r, c, this, new Future()) : then(f, r, this, new Future()) : n.trifurcate(f, r, c) + } + // toString :: Promise e a -> String toString () { return '[object ' + this.inspect() + ']' @@ -260,6 +266,10 @@ class Fulfilled extends Core { return fin(f, this, new Future()) } + trifurcate (f, r, c) { + return typeof f === 'function' ? then(f, undefined, this, new Future()) : this + } + toString () { return '[object ' + this.inspect() + ']' } @@ -327,6 +337,10 @@ class Rejected extends Core { return fin(f, this, new Future()) } + trifurcate (f, r, c) { + return this.token ? trifurcate(undefined, r, c, this, new Future()) : typeof r === 'function' ? then(undefined, r, this, new Future()) : this + } + toString () { return '[object ' + this.inspect() + ']' } @@ -390,6 +404,10 @@ class Never extends Core { return this } + trifurcate (f, r, c) { + return this + } + toString () { return '[object ' + this.inspect() + ']' } diff --git a/src/trifurcate.js b/src/trifurcate.js new file mode 100644 index 0000000..a3749f3 --- /dev/null +++ b/src/trifurcate.js @@ -0,0 +1,59 @@ +import Action from './Action' + +export default function trifurcate (f, r, c, p, promise) { + // assert: promise.token == null + // assert: p.token != null + p._when(new Trifurcation(f, r, c, p.token, promise)) + return promise +} + +class Trifurcation extends Action { + constructor (f, r, c, t, promise) { + super(promise) + this.token = t + t._subscribe(this) + this.f = f + this.r = r + this.c = c + } + + /* istanbul ignore next */ + destroy () { // possibly called when unsubscribed from the token + this.token = null + } + + cancel (p) { + /* istanbul ignore if */ + if (this.token == null) return + this.runTee(this.c, p.near()) + } + + fulfilled (p) { + this.token._unsubscribe(this) + this.runTee(this.f, p) + } + + rejected (p) { + this.token._unsubscribe(this) + return this.runTee(this.r, p) + } + + runTee (f, p) { + this.token = null + this.f = null + this.r = null + this.c = null + const hasHandler = typeof f === 'function' + if (hasHandler) { + this.tryCall(f, p.value) + } else { + this.put(p) + } + this.promise = null + return hasHandler + } + + handle (result) { + this.promise._resolve(result) + } +} diff --git a/test/fulfill-test.js b/test/fulfill-test.js index 8a7fc88..aad8154 100644 --- a/test/fulfill-test.js +++ b/test/fulfill-test.js @@ -80,4 +80,9 @@ describe('fulfill', () => { cancel({}) return assertSame(token.getRejected(), p.chain(assert.ifError, token)) }) + + it('trifurcate should be identity without f callback', () => { + const p = fulfill(true) + assert.strictEqual(p, p.trifurcate(undefined, assert.ifError, assert.ifError)) + }) }) diff --git a/test/never-test.js b/test/never-test.js index 9cdcbd9..5cb8cfb 100644 --- a/test/never-test.js +++ b/test/never-test.js @@ -63,6 +63,11 @@ describe('never', () => { assert.strictEqual(p, p.finally(() => {})) }) + it('trifurcate should be identity', () => { + const p = never() + assert.strictEqual(p, p.trifurcate(assert.ifError, assert.ifError, assert.ifError)) + }) + it('_when should not call action', () => { let fail = () => { throw new Error('never._when called action') } let action = { diff --git a/test/reject-test.js b/test/reject-test.js index 3cdde6b..8807509 100644 --- a/test/reject-test.js +++ b/test/reject-test.js @@ -87,4 +87,10 @@ describe('reject', () => { cancel({}) return assertSame(token.getRejected(), p.chain(assert.ifError, token)) }) + + it('trifurcate should be identity without r callback', () => { + const p = reject(true) + silenceError(p) + assert.strictEqual(p, p.trifurcate(assert.ifError, undefined, assert.ifError)) + }) }) diff --git a/test/trifurcate-test.js b/test/trifurcate-test.js new file mode 100644 index 0000000..80dcad2 --- /dev/null +++ b/test/trifurcate-test.js @@ -0,0 +1,444 @@ +import { describe, it } from 'mocha' +import { future, fulfill, reject, never, CancelToken, all } from '../src/main' +import { silentReject } from '../src/Promise' +import { assertSame, raceCallbacks } from './lib/test-util' +import assert from 'assert' + +describe('untilCancel', () => { + it('should always return a promise with the token on its .token property', () => { + const {token, cancel} = CancelToken.source() + + assert.strictEqual(never().untilCancel(token).token, token, 'never') + assert.strictEqual(future().promise.untilCancel(token).token, token, 'unresolved future') + assert.strictEqual(future(token).promise.untilCancel(token).token, token, 'unresolved future with same token') + assert.strictEqual(future(CancelToken.empty()).promise.untilCancel(token).token, token, 'unresolved future with other token') + + cancel({}) + + assert.strictEqual(fulfill().untilCancel(token).token, token, 'fulfill') + assert.strictEqual(reject().untilCancel(token).token, token, 'reject') + assert.strictEqual(never().untilCancel(token).token, token, 'never') + assert.strictEqual(future().promise.untilCancel(token).token, token, 'unresolved future') + const a = future() + a.resolve(fulfill()) + assert.strictEqual(a.promise.untilCancel(token).token, token, 'fulfilled future') + const b = future() + b.resolve(reject()) + assert.strictEqual(b.promise.untilCancel(token).token, token, 'rejected future') + const c = future(token) + assert.strictEqual(c.promise.untilCancel(token).token, token, 'future with token') + }) +}) + +describe('trifurcate', () => { + const ful = () => 'f' + const rej = () => 'r' + const can = () => 'c' + it('should behave like then without a token', () => { + const a = future() + const b = future() + const c = future() + const d = future() + a.resolve(fulfill()) + b.resolve(reject()) + const res = all([ + assertSame(fulfill().trifurcate(ful, rej, can), fulfill().then(ful, rej)), + assertSame( reject().trifurcate(ful, rej, can), reject().then(ful, rej)), + assertSame(a.promise.trifurcate(ful, rej, can), a.promise.then(ful, rej)), + assertSame(b.promise.trifurcate(ful, rej, can), b.promise.then(ful, rej)), + assertSame(c.promise.trifurcate(ful, rej, can), c.promise.then(ful, rej)), + assertSame(d.promise.trifurcate(ful, rej, can), d.promise.then(ful, rej)) + ]) + c.resolve(fulfill()) + d.resolve(reject()) + return res + }) + + const f = x => x + 1 + const fp = x => fulfill(x + 1) + const rp = x => silentReject(x + 1) + const tr = x => { throw x + 1 } + + describe('on fulfilled future', () => { + it('should only call the onfulfilled callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { resolve, promise } = future(CancelToken.empty()) + resolve(fulfill(1)) + promise.trifurcate(ok, nok, nok) + return result + }) + + it('should behave like the input without callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + resolve(p) + return assertSame(p, promise.trifurcate(undefined, assert.ifError, assert.ifError)) + }) + + it('should behave like map', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + resolve(p) + return assertSame(p.map(f), promise.trifurcate(f, assert.ifError, assert.ifError)) + }) + + it('should behave like chain with fulfillment', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + resolve(p) + return assertSame(p.chain(fp), promise.trifurcate(fp, assert.ifError, assert.ifError)) + }) + + it('should behave like chain with rejection', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + resolve(p) + return assertSame(p.chain(rp), promise.trifurcate(rp, assert.ifError, assert.ifError)) + }) + + it('should behave like then with exception', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + resolve(p) + return assertSame(p.then(tr), promise.trifurcate(tr, assert.ifError, assert.ifError)) + }) + }) + + describe('on rejected future', () => { + it('should only call the onrejected callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { resolve, promise } = future(CancelToken.empty()) + resolve(reject(1)) + promise.trifurcate(nok, ok, nok) + return result + }) + + it('should behave like the input without callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + resolve(p) + return assertSame(p, promise.trifurcate(assert.ifError, undefined, assert.ifError)) + }) + + it('should behave like catch', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + resolve(p) + return assertSame(p.catch(f), promise.trifurcate(assert.ifError, f, assert.ifError)) + }) + + it('should behave like catch with fulfillment', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + resolve(p) + return assertSame(p.catch(fp), promise.trifurcate(assert.ifError, fp, assert.ifError)) + }) + + it('should behave like catch with rejection', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + resolve(p) + return assertSame(p.catch(rp), promise.trifurcate(assert.ifError, rp, assert.ifError)) + }) + + it('should behave like catch with exception', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + resolve(p) + return assertSame(p.catch(tr), promise.trifurcate(assert.ifError, tr, assert.ifError)) + }) + }) + + describe('on cancelled future', () => { + it('should only call the oncancelled callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + promise.trifurcate(nok, nok, ok) + return result + }) + + it('should behave like getRejected without callback', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + return assertSame(token.getRejected(), promise.trifurcate(assert.ifError, assert.ifError, undefined)) + }) + + it('should behave like subscribe', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + return assertSame(token.subscribe(f), promise.trifurcate(assert.ifError, assert.ifError, f)) + }) + + it('should behave like subscribe with fulfillment', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + return assertSame(token.subscribe(fp), promise.trifurcate(assert.ifError, assert.ifError, fp)) + }) + + it('should behave like subscribe with rejection', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + return assertSame(token.subscribe(rp), promise.trifurcate(assert.ifError, assert.ifError, rp)) + }) + + it('should behave like subscribe with exception', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + cancel(1) + return assertSame(token.subscribe(tr), promise.trifurcate(assert.ifError, assert.ifError, tr)) + }) + }) + + describe('on future before fulfilled', () => { + it('should only call the onfulfilled callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { resolve, promise } = future(CancelToken.empty()) + promise.trifurcate(ok, nok, nok) + resolve(fulfill(1)) + return result + }) + + it('should behave like the input without callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + const res = promise.trifurcate(undefined, assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p) + }) + + it('should behave like map', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + const res = promise.trifurcate(f, assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.map(f)) + }) + + it('should behave like chain with fulfillment', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + const res = promise.trifurcate(fp, assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.chain(fp)) + }) + + it('should behave like chain with rejection', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + const res = promise.trifurcate(rp, assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.chain(rp)) + }) + + it('should behave like then with exception', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = fulfill(1) + const res = promise.trifurcate(tr, assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.then(tr)) + }) + }) + + describe('on future before rejected', () => { + it('should only call the onrejected callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { resolve, promise } = future(CancelToken.empty()) + promise.trifurcate(nok, ok, nok) + resolve(reject(1)) + return result + }) + + it('should behave like the input without callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, undefined, assert.ifError) + resolve(p) + return assertSame(res, p) + }) + + it('should behave like catch', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, f, assert.ifError) + resolve(p) + return assertSame(res, p.catch(f)) + }) + + it('should behave like catch with fulfillment', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, fp, assert.ifError) + resolve(p) + return assertSame(res, p.catch(fp)) + }) + + it('should behave like catch with rejection', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, rp, assert.ifError) + resolve(p) + return assertSame(res, p.catch(rp)) + }) + + it('should behave like catch with exception', () => { + const { resolve, promise } = future(CancelToken.empty()) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, tr, assert.ifError) + resolve(p) + return assertSame(res, p.catch(tr)) + }) + }) + + describe('on future before cancelled', () => { + it('should only call the oncancelled callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + promise.trifurcate(nok, nok, ok) + cancel(1) + return result + }) + + it('should behave like getRejected without callback', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const res = promise.trifurcate(assert.ifError, assert.ifError, undefined) + cancel(1) + return assertSame(res, token.getRejected()) + }) + + it('should behave like subscribe', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const res = promise.trifurcate(assert.ifError, assert.ifError, f) + cancel(1) + return assertSame(res, token.subscribe(f)) + }) + + it('should behave like subscribe with fulfillment', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const res = promise.trifurcate(assert.ifError, assert.ifError, fp) + cancel(1) + return assertSame(res, token.subscribe(fp)) + }) + + it('should behave like subscribe with rejection', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const res = promise.trifurcate(assert.ifError, assert.ifError, rp) + cancel(1) + return assertSame(res, token.subscribe(rp)) + }) + + it('should behave like subscribe with exception', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const res = promise.trifurcate(assert.ifError, assert.ifError, tr) + cancel(1) + return assertSame(res, token.subscribe(tr)) + }) + }) + + const pre = (f, g) => x => (f({}), g(x)) + + describe('on future before fulfilled even when cancelled from the callback', () => { + it('should only call the onfulfilled callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + promise.trifurcate(pre(cancel, ok), nok, nok) + resolve(fulfill(1)) + return result + }) + + it('should behave like map', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = fulfill(1) + const res = promise.trifurcate(pre(cancel, f), assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.map(f)) + }) + + it('should behave like chain with fulfillment', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = fulfill(1) + const res = promise.trifurcate(pre(cancel, fp), assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.chain(fp)) + }) + + it('should behave like chain with rejection', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = fulfill(1) + const res = promise.trifurcate(pre(cancel, rp), assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.chain(rp)) + }) + + it('should behave like then with exception', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = fulfill(1) + const res = promise.trifurcate(pre(cancel, tr), assert.ifError, assert.ifError) + resolve(p) + return assertSame(res, p.then(tr)) + }) + }) + + describe('on future before rejected even when cancelled from the callback', () => { + it('should only call the onrejected callback', () => { + const { ok, nok, result } = raceCallbacks(future) + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + promise.trifurcate(nok, pre(cancel, ok), nok) + resolve(reject(1)) + return result + }) + + it('should behave like catch', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, pre(cancel, f), assert.ifError) + resolve(p) + return assertSame(res, p.catch(f)) + }) + + it('should behave like catch with fulfillment', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, pre(cancel, fp), assert.ifError) + resolve(p) + return assertSame(res, p.catch(fp)) + }) + + it('should behave like catch with rejection', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, pre(cancel, rp), assert.ifError) + resolve(p) + return assertSame(res, p.catch(rp)) + }) + + it('should behave like catch with exception', () => { + const { token, cancel } = CancelToken.source() + const { resolve, promise } = future(token) + const p = reject(1) + const res = promise.trifurcate(assert.ifError, pre(cancel, tr), assert.ifError) + resolve(p) + return assertSame(res, p.catch(tr)) + }) + }) +}) From eb0000b71f4ea52dae552d4da0f6a60a817e87d4 Mon Sep 17 00:00:00 2001 From: Bergi Date: Thu, 7 Jul 2016 05:30:39 +0200 Subject: [PATCH 24/28] more tests and problems lots of new cancellation tests, some of which are failing --- test/CancelToken-test.js | 4 +- test/Promise-test.js | 31 ++- test/cancellation-test.js | 556 +++++++++++++++++++++++++++++++------- test/chain-test.js | 5 + test/coroutine-test.js | 7 +- test/future-test.js | 60 +++- test/resolve-test.js | 52 +++- test/then-test.js | 5 + test/trifurcate-test.js | 83 +++++- 9 files changed, 685 insertions(+), 118 deletions(-) diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index d73f0d4..24b7935 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -345,7 +345,7 @@ describe('CancelToken', function () { assert.strictEqual(called, 2) }) - it('should invoke f if the token is cancelled during the call', () => { + it('should only invoke f if the call happens during the cancellation', () => { const {token, cancel} = CancelToken.source() let called = 0 const call = token.subscribeOrCall(() => { called |= 1; call() }, () => { called |= 2 }) @@ -354,7 +354,7 @@ describe('CancelToken', function () { assert.strictEqual(called, 1) }) - it('should invoke g if the call happens during the cancellation', () => { + it('should only invoke g if the token is cancelled during the call', () => { const {token, cancel} = CancelToken.source() let called = 0 const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2; cancel() }) diff --git a/test/Promise-test.js b/test/Promise-test.js index f3ed0ad..4ac7bd9 100644 --- a/test/Promise-test.js +++ b/test/Promise-test.js @@ -14,13 +14,14 @@ describe('Promise', () => { return p }) - it('should not call executor when token is cancelled', () => { + it('should not call executor and immediately reject when token is requested', () => { const {token, cancel} = CancelToken.source() cancel({}) let called = false const p = new Promise((resolve, reject) => { called = true }, token) + assert(isRejected(p)) assert(!called) return assertSame(token.getRejected(), p) }) @@ -79,6 +80,34 @@ describe('Promise', () => { return new Promise((resolve, reject) => setTimeout(reject, 1, reject(expected))) .then(assert.ifError, x => assert.strictEqual(expected, x)) }) + + it('should not change state when called multiple times', () => { + let res, rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }) + res(1) + const expected = promise.state() + res(2) + assert.strictEqual(promise.state(), expected) + rej(3) + assert.strictEqual(promise.state(), expected) + }) + + it('should not change state with token when called multiple times', () => { + let res, rej + const promise = new Promise((resolve, reject) => { + res = resolve + rej = reject + }, CancelToken.empty()) + res(1) + const expected = promise.state() + res(2) + assert.strictEqual(promise.state(), expected) + rej(3) + assert.strictEqual(promise.state(), expected) + }) }) describe('token', () => { diff --git a/test/cancellation-test.js b/test/cancellation-test.js index 0615f88..f423a13 100644 --- a/test/cancellation-test.js +++ b/test/cancellation-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { future, reject, fulfill, never, isRejected, CancelToken } from '../src/main' +import { future, reject, fulfill, never, delay, isRejected, CancelToken } from '../src/main' import { silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -413,6 +413,272 @@ describe('reject', () => { }) describe('future', () => { + describe('with token', () => { + it('should reject the future when cancelled', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const expected = {} + const res = promise.catch(e => assert.strictEqual(e, expected)) + cancel(expected) + return res + }) + + it('should behave like cancellation before cancelled for no-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future() + a.resolve(b.promise) + const res = assertSame(a.promise, token.getRejected()) + cancel({}) + return res + }) + + it('should behave like cancellation after cancelled for no-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future() + a.resolve(b.promise) + cancel({}) + return assertSame(a.promise, token.getRejected()) + }) + + it('should behave like cancellation before cancelled for same-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + const res = assertSame(a.promise, token.getRejected()) + cancel({}) + return res + }) + + it('should behave like cancellation after cancelled for same-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + cancel({}) + return assertSame(a.promise, token.getRejected()) + }) + + it('should behave like cancellation before cancelled for different-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const res = assertSame(a.promise, token.getRejected()) + cancel({}) + return res + }) + + it('should behave like cancellation after cancelled for different-token resolution', () => { + const { token, cancel } = CancelToken.source() + const a = future(token) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + cancel({}) + return assertSame(a.promise, token.getRejected()) + }) + + it('should behave like rejection before resolution cancelled for no token', () => { + const { token, cancel } = CancelToken.source() + const a = future() + const b = future(token) + a.resolve(b.promise) + const expected = {} + const res = assertSame(a.promise, reject(expected)) + cancel(expected) + return res + }) + + it('should behave like rejection after resolution cancelled for no token', () => { + const { token, cancel } = CancelToken.source() + const a = future() + const b = future(token) + a.resolve(b.promise) + const expected = {} + cancel(expected) + return assertSame(a.promise, reject(expected)) + }) + + it('should behave like rejection before resolution cancelled for different token', () => { + const { token, cancel } = CancelToken.source() + const a = future(CancelToken.empty()) + const b = future(token) + a.resolve(b.promise) + const expected = {} + const res = assertSame(a.promise, reject(expected)) + cancel(expected) + return res + }) + + it('should behave like rejection after resolution cancelled for different token', () => { + const { token, cancel } = CancelToken.source() + const a = future(CancelToken.empty()) + const b = future(token) + a.resolve(b.promise) + const expected = {} + cancel(expected) + return assertSame(a.promise, reject(expected)) + }) + + it('should behave like fulfillment before no-token resolution fulfilled', () => { + const a = future(CancelToken.empty()) + const b = future() + a.resolve(b.promise) + const expected = fulfill({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like fulfillment after no-token resolution fulfilled', () => { + const a = future(CancelToken.empty()) + const b = future() + a.resolve(b.promise) + const expected = fulfill({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like fulfillment before same-token resolution fulfilled', () => { + const token = CancelToken.empty() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + const expected = fulfill({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like fulfillment after same-token resolution fulfilled', () => { + const token = CancelToken.empty() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + const expected = fulfill({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like fulfillment before different-token resolution fulfilled', () => { + const a = future(CancelToken.empty()) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = fulfill({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like fulfillment after different-token resolution fulfilled', () => { + const a = future(CancelToken.empty()) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = fulfill({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like fulfillment before some-token resolution fulfilled', () => { + const a = future() + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = fulfill({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like fulfillment after some-token resolution fulfilled', () => { + const a = future() + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = fulfill({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like rejection before no-token resolution rejected', () => { + const a = future(CancelToken.empty()) + const b = future() + a.resolve(b.promise) + const expected = reject({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like rejection after no-token resolution rejected', () => { + const a = future(CancelToken.empty()) + const b = future() + a.resolve(b.promise) + const expected = reject({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like rejection before same-token resolution rejected', () => { + const token = CancelToken.empty() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + const expected = reject({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like rejection after same-token resolution rejected', () => { + const token = CancelToken.empty() + const a = future(token) + const b = future(token) + a.resolve(b.promise) + const expected = reject({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like rejection before different-token resolution rejected', () => { + const a = future(CancelToken.empty()) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = reject({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like rejection after different-token resolution rejected', () => { + const a = future(CancelToken.empty()) + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = reject({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + + it('should behave like rejection before some-token resolution rejected', () => { + const a = future() + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = reject({}) + const res = assertSame(a.promise, expected) + b.resolve(expected) + return res + }) + + it('should behave like rejection after some-token resolution rejected', () => { + const a = future() + const b = future(CancelToken.empty()) + a.resolve(b.promise) + const expected = reject({}) + b.resolve(expected) + return assertSame(a.promise, expected) + }) + }) + describe('then without callbacks', () => { it('should behave like cancellation when cancelled', () => { const { token, cancel } = CancelToken.source() @@ -1114,109 +1380,7 @@ describe('future', () => { }) describe('when being cancelled from the handler', () => { - /* alternative proposal - const pre = (f, g) => x => (f({}), g(x)) - describe('then', () => { - it('should behave like mapped for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.then(pre(cancel, f), null, token) - resolve(p) - return assertSame(p.map(f), res) - }) - - it('should behave like chained for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.then(pre(cancel, fp), null, token) - resolve(p) - return assertSame(p.chain(fp), res) - }) - - it('should behave like rejection chained for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.then(pre(cancel, rp), null, token) - resolve(p) - return assertSame(p.chain(rp), res) - }) - }) - - describe('catch', () => { - it('should behave like mapped for reject', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = reject(1) - const res = promise.catch(pre(cancel, f), token) - resolve(p) - return assertSame(p.catch(f), res) - }) - - it('should behave like chained for reject', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = reject(1) - const res = promise.catch(pre(cancel, fp), token) - resolve(p) - return assertSame(p.catch(fp), res) - }) - - it('should behave like rejection chained for reject', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = reject(1) - const res = promise.catch(pre(cancel, rp), token) - resolve(p) - return assertSame(p.catch(rp), res) - }) - }) - - describe('map', () => { - it('should behave like mapped for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.map(pre(cancel, f), token) - resolve(p) - return assertSame(p.map(f), res) - }) - }) - - describe('ap', () => { - it('should behave like apply for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(pre(cancel, f)) - const q = fulfill(1) - const res = promise.ap(q, token) - resolve(p) - return assertSame(fulfill(f).ap(q), res) - }) - }) - - describe('chain', () => { - it('should behave like chained for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.chain(pre(cancel, fp), token) - resolve(p) - return assertSame(p.chain(fp), res) - }) - - it('should behave like rejection chained for fulfill', () => { - const { token, cancel } = CancelToken.source() - const { resolve, promise } = future() - const p = fulfill(1) - const res = promise.chain(pre(cancel, rp), token) - resolve(p) - return assertSame(p.chain(rp), res) - }) - }) */ - + // see also how trifurcate handles this differently describe('then', () => { it('should behave like cancellation and ignore exceptions for fulfill', () => { const { token, cancel } = CancelToken.source() @@ -1442,3 +1606,187 @@ describe('future', () => { }) }) }) + +describe('then with nested callbacks', () => { + it('should near to inner when both have the same token', () => { + const { token } = CancelToken.source() + const expected = fulfill({}) + const p = delay(3, expected, token) + const q = delay(1) + const r = q.then(() => p, undefined, token) + return q.then(() => { + assert.strictEqual(r.near(), p) + return assertSame(expected, r) + }) + }) + + it('should behave like cancellation when the outer promise is cancelled for no inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.then(() => delay(1), undefined, token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation when the outer promise is cancelled for same inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.then(() => delay(1, null, token), undefined, token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation when the outer promise is cancelled for different inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.then(() => delay(1, null, CancelToken.empty()), undefined, token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like fulfillment for no inner token', () => { + const expected = fulfill({}) + const res = delay(1).then(() => delay(1, expected), undefined, CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like fulfillment for same inner token', () => { + const token = CancelToken.empty() + const expected = fulfill({}) + const res = delay(1).then(() => delay(1, expected, token), undefined, token) + return assertSame(expected, res) + }) + + it('should behave like fulfillment for different inner token', () => { + const expected = fulfill({}) + const res = delay(1).then(() => delay(1, expected, CancelToken.empty()), undefined, CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection for no inner token', () => { + const expected = reject({}) + const res = delay(1).then(() => delay(1, expected), undefined, CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection for same inner token', () => { + const token = CancelToken.empty() + const expected = reject({}) + const res = delay(1).then(() => delay(1, expected, token), undefined, token) + return assertSame(expected, res) + }) + + it('should behave like rejection for different inner token', () => { + const expected = reject({}) + const res = delay(1).then(() => delay(1, expected, CancelToken.empty()), undefined, CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection when the inner promise is cancelled for no outer token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.then(() => delay(1, null, token)) + p.then(cancel) + return assertSame(p.then(reject), res) + }) + + it('should behave like rejection when the inner promise is cancelled for different outer token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.then(() => delay(1, null, token), undefined, CancelToken.empty()) + p.then(cancel) + return assertSame(p.then(reject), res) + }) +}) + +describe('chain with nested callbacks', () => { + it('should near to inner when both have the same token', () => { + const { token } = CancelToken.source() + const expected = fulfill({}) + const p = delay(3, expected, token) + const q = delay(1) + const r = q.chain(() => p, token) + return q.then(() => { + assert.strictEqual(r.near(), p) + return assertSame(expected, r) + }) + }) + + it('should behave like cancellation when the outer promise is cancelled for no inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.chain(() => delay(1), token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation when the outer promise is cancelled for same inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.chain(() => delay(1, null, token), token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like cancellation when the outer promise is cancelled for different inner token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.chain(() => delay(1, null, CancelToken.empty()), token) + p.then(cancel) + return assertSame(token.getRejected(), res) + }) + + it('should behave like fulfillment for no inner token', () => { + const expected = fulfill({}) + const res = delay(1).chain(() => delay(1, expected), CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like fulfillment for same inner token', () => { + const token = CancelToken.empty() + const expected = fulfill({}) + const res = delay(1).chain(() => delay(1, expected, token), token) + return assertSame(expected, res) + }) + + it('should behave like fulfillment for different inner token', () => { + const expected = fulfill({}) + const res = delay(1).chain(() => delay(1, expected, CancelToken.empty()), CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection for no inner token', () => { + const expected = reject({}) + const res = delay(1).chain(() => delay(1, expected), CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection for same inner token', () => { + const token = CancelToken.empty() + const expected = reject({}) + const res = delay(1).chain(() => delay(1, expected, token), token) + return assertSame(expected, res) + }) + + it('should behave like rejection for different inner token', () => { + const expected = reject({}) + const res = delay(1).chain(() => delay(1, expected, CancelToken.empty()), CancelToken.empty()) + return assertSame(expected, res) + }) + + it('should behave like rejection when the inner promise is cancelled for no outer token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.chain(() => delay(1, null, token)) + p.then(cancel) + return assertSame(p.then(reject), res) + }) + + it('should behave like rejection when the inner promise is cancelled for different outer token', () => { + const { token, cancel } = CancelToken.source() + const p = delay(1, {}) + const res = p.chain(() => delay(1, null, token), CancelToken.empty()) + p.then(cancel) + return assertSame(p.then(reject), res) + }) +}) diff --git a/test/chain-test.js b/test/chain-test.js index 3f204cd..76c5cef 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -29,4 +29,9 @@ describe('chain', function () { return delay(1, expected).then(reject).chain(() => null) .then(assert.ifError, x => assert.strictEqual(x, expected)) }) + + it('should have cycle detection', () => { + const p = delay(1).chain(() => p) + return p.then(assert.ifError, e => assert(e instanceof TypeError)) + }) }) diff --git a/test/coroutine-test.js b/test/coroutine-test.js index 9e310f8..b7ad769 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -195,11 +195,10 @@ describe('coroutine', function () { it('should return the token of the result promise', () => { const p = coroutine(function* () { - return coroutine.cancel + yield delay(1) + assert.strictEqual(coroutine.cancel, p.token) })() - return p.then(token => { - assert.strictEqual(token, p.token) - }) + return p }) it('should not be available outside a coroutine', () => { diff --git a/test/future-test.js b/test/future-test.js index 158b2fd..d05ef8b 100644 --- a/test/future-test.js +++ b/test/future-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { future, reject, fulfill, isSettled, isPending, never } from '../src/main' +import { future, reject, fulfill, isSettled, isPending, never, CancelToken } from '../src/main' import { Future, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -25,6 +25,64 @@ describe('future', () => { }) }) + describe('state', () => { + it('should not change after resolution', () => { + const { resolve, promise } = future() + resolve(fulfill()) + const expected = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), expected) + resolve(reject()) + assert.strictEqual(promise.state(), expected) + }) + + it('should not change after resolution with future', () => { + const { resolve, promise } = future() + const f = future() + resolve(f.promise) + const expected = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), expected) + resolve(reject()) + assert.strictEqual(promise.state(), expected) + + f.resolve(reject()) + const rejected = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), rejected) + resolve(reject()) + assert.strictEqual(promise.state(), rejected) + }) + + it('should not change with token after resolution', () => { + const { resolve, promise } = future(CancelToken.empty()) + resolve(reject()) + const expected = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), expected) + resolve(reject()) + assert.strictEqual(promise.state(), expected) + }) + + it('should not change with token after resolution with future', () => { + const { resolve, promise } = future(CancelToken.empty()) + const f = future() + resolve(f.promise) + const expected = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), expected) + resolve(reject()) + assert.strictEqual(promise.state(), expected) + + f.resolve(fulfill()) + const fulfilled = promise.state() + resolve(fulfill()) + assert.strictEqual(promise.state(), fulfilled) + resolve(reject()) + assert.strictEqual(promise.state(), fulfilled) + }) + }) + describe('resolve', () => { it('should fulfill promise with value', () => { const { resolve, promise } = future() diff --git a/test/resolve-test.js b/test/resolve-test.js index 12f93ab..abef094 100644 --- a/test/resolve-test.js +++ b/test/resolve-test.js @@ -1,6 +1,7 @@ import { describe, it } from 'mocha' -import { resolve, CancelToken } from '../src/main' +import { resolve, fulfill, reject, future, isRejected, CancelToken } from '../src/main' import { Future } from '../src/Promise' +import { assertSame } from './lib/test-util' import assert from 'assert' describe('resolve', () => { @@ -68,7 +69,9 @@ describe('resolve', () => { }) return p }) + }) + describe('token', () => { it('should return cancellation with cancelled token for true', () => { const {token, cancel} = CancelToken.source() cancel({}) @@ -81,10 +84,57 @@ describe('resolve', () => { assert.strictEqual(token.getRejected(), resolve(new Future(), token)) }) + it('should return cancellation with cancelled token for fulfill', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + assert.strictEqual(token.getRejected(), resolve(fulfill(), token)) + }) + + it('should return cancellation with cancelled token for reject', () => { + const {token, cancel} = CancelToken.source() + cancel({}) + assert.strictEqual(token.getRejected(), resolve(reject(), token)) + }) + it('should be identity for future with same token', () => { const {token} = CancelToken.source() const p = new Future(token) assert.strictEqual(p, resolve(p, token)) }) + + it('should cancel result for unresolved promise', () => { + const {token, cancel} = CancelToken.source() + const {promise} = future() + const p = resolve(promise, token) + cancel({}) + assert(!isRejected(promise)) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + + it('should cancel result for unresolved promise with different token', () => { + const {token, cancel} = CancelToken.source() + const {promise} = future(CancelToken.empty()) + const p = resolve(promise, token) + cancel({}) + assert(!isRejected(promise)) + assert(isRejected(p)) + return assertSame(token.getRejected(), p) + }) + }) + + it('should be identity for fulfilled promise', () => { + const p = fulfill() + assert.strictEqual(resolve(p), p) + }) + + it('should be identity for rejected promise', () => { + const p = reject() + assert.strictEqual(resolve(p), p) + }) + + it('should be identity for unresolved promise', () => { + const p = future().promise + assert.strictEqual(resolve(p), p) }) }) diff --git a/test/then-test.js b/test/then-test.js index 0132906..4c295d6 100644 --- a/test/then-test.js +++ b/test/then-test.js @@ -26,4 +26,9 @@ describe('then', function () { return delay(1).then(reject).then(null, () => { throw expected }) .then(assert.ifError, x => assert.strictEqual(x, expected)) }) + + it('should have cycle detection', () => { + const p = delay(1).then(() => p) + return p.then(assert.ifError, e => assert(e instanceof TypeError)) + }) }) diff --git a/test/trifurcate-test.js b/test/trifurcate-test.js index 80dcad2..e0204e2 100644 --- a/test/trifurcate-test.js +++ b/test/trifurcate-test.js @@ -68,6 +68,17 @@ describe('trifurcate', () => { return result }) + it('should asynchronously call the callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + resolve(fulfill(1)) + let called = false + const res = promise.trifurcate(() => { + called = true + }) + assert(!called) + return res.then(() => assert(called)) + }) + it('should behave like the input without callback', () => { const { resolve, promise } = future(CancelToken.empty()) const p = fulfill(1) @@ -113,6 +124,17 @@ describe('trifurcate', () => { return result }) + it('should asynchronously call the callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + resolve(reject(1)) + let called = false + const res = promise.trifurcate(undefined, () => { + called = true + }) + assert(!called) + return res.then(() => assert(called)) + }) + it('should behave like the input without callback', () => { const { resolve, promise } = future(CancelToken.empty()) const p = reject(1) @@ -159,11 +181,24 @@ describe('trifurcate', () => { return result }) - it('should behave like getRejected without callback', () => { + it('should asynchronously call the callback', () => { const { token, cancel } = CancelToken.source() const { promise } = future(token) cancel(1) - return assertSame(token.getRejected(), promise.trifurcate(assert.ifError, assert.ifError, undefined)) + let called = false + const res = promise.trifurcate(undefined, undefined, () => { + called = true + }) + assert(!called) + return res.then(() => assert(called)) + }) + + it('should behave like rejected without callback', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const expected = {} + cancel(expected) + return assertSame(reject(expected), promise.trifurcate(assert.ifError, assert.ifError, undefined)) }) it('should behave like subscribe', () => { @@ -204,6 +239,18 @@ describe('trifurcate', () => { return result }) + it('should asynchronously call the callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + let called = false + const res = promise.trifurcate(() => { + called = true + }) + assert(!called) + resolve(fulfill(1)) + assert(!called) + return res.then(() => assert(called)) + }) + it('should behave like the input without callback', () => { const { resolve, promise } = future(CancelToken.empty()) const p = fulfill(1) @@ -254,6 +301,18 @@ describe('trifurcate', () => { return result }) + it('should asynchronously call the callback', () => { + const { resolve, promise } = future(CancelToken.empty()) + let called = false + const res = promise.trifurcate(undefined, () => { + called = true + }) + assert(!called) + resolve(reject(1)) + assert(!called) + return res.then(() => assert(called)) + }) + it('should behave like the input without callback', () => { const { resolve, promise } = future(CancelToken.empty()) const p = reject(1) @@ -305,12 +364,26 @@ describe('trifurcate', () => { return result }) - it('should behave like getRejected without callback', () => { + it('should asynchronously call the callback', () => { const { token, cancel } = CancelToken.source() const { promise } = future(token) - const res = promise.trifurcate(assert.ifError, assert.ifError, undefined) + let called = false + const res = promise.trifurcate(undefined, undefined, () => { + called = true + }) + assert(!called) cancel(1) - return assertSame(res, token.getRejected()) + assert(!called) + return res.then(() => assert(called)) + }) + + it('should behave like rejected without callback', () => { + const { token, cancel } = CancelToken.source() + const { promise } = future(token) + const expected = {} + const res = promise.trifurcate(assert.ifError, assert.ifError, undefined) + cancel(expected) + return assertSame(res, reject(expected)) }) it('should behave like subscribe', () => { From 47a022ffee23fd199e644e5535f67ebd4831a1aa Mon Sep 17 00:00:00 2001 From: Bergi Date: Sat, 9 Jul 2016 00:35:23 +0200 Subject: [PATCH 25/28] extra cancelled state as a special case of rejection * new Cancelled type that behaves like Rejected everywhere except for trifurcate * token is now private again and vanishes on resolution or settlement * proper support for tokens in future() and Promise() * consistently unsubscribe from tokens on settlement * further simplify (?) Actions by abstracting more things in a CancellableAction class * optmise _resolve() for token cancellation between resolution and settlement * renamed CancelToken::getRejected to getCancelled * fix some cases where Rejections were not tracked --- src/Action.js | 107 ++++++++++++----- src/CancelToken.js | 16 +-- src/Promise.js | 240 +++++++++++++++++++++++++++----------- src/chain.js | 26 +---- src/combinators.js | 1 + src/coroutine.js | 6 +- src/finally.js | 60 +++++----- src/inspect.js | 6 +- src/main.js | 53 +++++---- src/map.js | 20 +--- src/runPromise.js | 12 +- src/state.js | 3 +- src/subscribe.js | 27 +---- src/then.js | 19 ++- src/trifurcate.js | 51 ++++---- test/CancelToken-test.js | 54 ++++----- test/Promise-test.js | 12 +- test/cancellation-test.js | 216 +++++++++++++++++----------------- test/coroutine-test.js | 14 +-- test/delay-test.js | 22 ++-- test/fulfill-test.js | 10 +- test/future-test.js | 9 +- test/inspect-test.js | 59 +++++++++- test/never-test.js | 10 +- test/reject-test.js | 10 +- test/resolve-test.js | 31 +++-- test/trifurcate-test.js | 61 ++++++---- 27 files changed, 666 insertions(+), 489 deletions(-) diff --git a/src/Action.js b/src/Action.js index 9e61eef..ce969a3 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,12 +1,12 @@ -import { Future } from './Promise' - -let sentinel = null -const empty = [] +import { noop } from './util' +import { Future, reject } from './Promise' export default class Action { constructor (promise) { - this.promise = promise // the Future which this Action tries to resolve - // when null, the action is cancelled and won't be executed + // the Future which this Action tries to resolve + // when null, the action is cancelled and won't be executed + this.promise = promise + const token = promise.token if (token != null) { // assert: !token.requested @@ -19,20 +19,13 @@ export default class Action { } cancel (p) { + /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself - if (this.promise === sentinel) { - this.destroy() - this.promise = new Future() - return this.promise - } else { - this.destroy() - return empty - } + this.destroy() } } // default onFulfilled action - /* istanbul ignore next */ fulfilled (p) { this.put(p) } @@ -43,32 +36,82 @@ export default class Action { return false } + // default onCancelled action + cancelled (p) { + reject(p.value)._runAction(this) + } + + // when this.promise is to be settled (possible having awaited the result) + put (p) { + // assert: isSettled(p) || p.token === this.promise.token + // asssert: this.promise != null + this.end().__become(p) + } + + end () { + const promise = this.promise + const token = promise.token + this.promise = null + if (token != null) token._unsubscribe(this) + return promise + } +} + +const sentinel = noop // Symbol('currently executing') +const empty = [] + +export class CancellableAction extends Action { + constructor (f, promise) { + super(promise) + // the function that produces the resolution result for the promise + // when null, the function has been executed but the promise might still get cancelled + this.f = f + } + + destroy () { + this.promise = null + this.f = null + } + + cancel (p) { + if (this.promise._isResolved()) { // promise checks for cancellation itself + if (this.f === sentinel) { + this.destroy() + this.promise = new Future() // allow to relay feedback to the cancel() call + return this.promise // TODO: really useful? leaking callback results is weird + } else { + this.destroy() + return empty + } + } + } + + fulfilled (p) { + if (this.f) { + this.tryCall(this.f, p.value) + } else { + this.put(p) + } + } + tryCall (f, x) { - const original = sentinel = this.promise + const original = this.promise + this.f = sentinel let result try { result = f(x) } catch (e) { - sentinel = null - this.promise._reject(e) - return this.promise === original + this.f = null + const uncancelled = this.promise === original + this.end()._reject(e) + return uncancelled } - sentinel = null + this.f = null this.handle(result) return this.promise === original } - tryUnsubscribe () { - const token = this.promise.token - if (token != null) token._unsubscribe(this) - this.promise = null - } - - put (p) { - const promise = this.promise - const token = promise.token - promise._become(p) - if (token != null) token._unsubscribe(this) - this.promise = null + handle (p) { + this.promise._resolve(p, this) } } diff --git a/src/CancelToken.js b/src/CancelToken.js index 9a3b20e..0e2835d 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -1,5 +1,5 @@ import { noop } from './util' -import { Future, resolve, silentReject, taskQueue } from './Promise' // deferred +import { Future, resolve, cancel, taskQueue } from './Promise' // deferred import { subscribe, subscribeOrCall } from './subscribe' export default class CancelToken { @@ -19,7 +19,7 @@ export default class CancelToken { } _cancel (reason) { if (this._cancelled) return - return this.__cancel(silentReject(reason)) + return this.__cancel(cancel(reason)) } __cancel (p) { this._cancelled = true @@ -96,7 +96,7 @@ export default class CancelToken { subscribeOrCall (fn, c) { return subscribeOrCall(fn, c, this, new Future()) } - getRejected () { + getCancelled () { if (this.promise === void 0) { this.promise = new Future(this) // while not settled, provides a reference to token } @@ -205,7 +205,7 @@ class CancelTokenRace extends CancelTokenCombinator { continue } if (t.requested) { - this.cancel(t.getRejected()) + this.cancel(t.getCancelled()) break } else { this.tokens.push(t) @@ -232,9 +232,9 @@ class CancelTokenPool extends CancelTokenCombinator { } _check () { if (this.count === 0) { - const reasons = this.tokens.map(t => t.getRejected().near().value) + const reasons = this.tokens.map(t => t.getCancelled().near().value) this.tokens = null - return this.promise.__cancel(silentReject(reasons)) + return this.promise.__cancel(cancel(reasons)) } } add (...tokens) { @@ -267,7 +267,7 @@ export class CancelTokenReference extends CancelTokenCombinator { } cancel (p) { /* istanbul ignore if */ - if (this.curToken == null || this.curToken.getRejected() !== p) return // when called from an oldToken + if (this.curToken == null || this.curToken.getCancelled() !== p) return // when called from an oldToken // assert: !this.promise._cancelled return this.promise.__cancel(p) } @@ -287,7 +287,7 @@ export class CancelTokenReference extends CancelTokenCombinator { this.curToken = newToken if (newToken) { if (newToken.requested) { - this.promise.__cancel(newToken.getRejected()) + this.promise.__cancel(newToken.getCancelled()) } else { newToken._subscribe(this) } diff --git a/src/Promise.js b/src/Promise.js index 59e85e8..0ad3798 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -1,5 +1,5 @@ -import { isObject } from './util' -import { PENDING, FULFILLED, REJECTED, NEVER, HANDLED } from './state' +import { isObject, noop } from './util' +import { PENDING, FULFILLED, REJECTED, CANCELLED, NEVER, HANDLED } from './state' import { isRejected, isNever, isSettled } from './inspect' import { TaskQueue, Continuation } from './TaskQueue' @@ -39,6 +39,11 @@ class Core { static of (x) { return fulfill(x) } + + // toString :: Promise e a -> String + toString () { + return '[object ' + this.inspect() + ']' + } } // data Promise e a where @@ -54,7 +59,7 @@ export class Future extends Core { super() this.ref = void 0 this.action = void 0 - this.token = token != null ? CancelToken.from(token) : null + this.token = CancelToken.from(token) this.length = 0 } @@ -113,25 +118,22 @@ export class Future extends Core { } const p = new Future(token) if (p.token.requested) { - return p.token.getRejected() + return p.token.getCancelled() } this._runAction(new Action(p)) return p } + // finally :: Promise e a -> (Promise e a -> ()) -> Promise e a finally (f) { const n = this.near() return n === this ? fin(f, this, new Future()) : n.finally(f) } + // trifurcate :: Promise e a -> (a -> b) -> (e -> b) -> (e -> b) -> Promise e b trifurcate (f, r, c) { const n = this.near() - return n === this ? this.token ? trifurcate(f, r, c, this, new Future()) : then(f, r, this, new Future()) : n.trifurcate(f, r, c) - } - - // toString :: Promise e a -> String - toString () { - return '[object ' + this.inspect() + ']' + return n === this ? trifurcate(f, r, c, this, new Future()) : n.trifurcate(f, r, c) } // inspect :: Promise e a -> String @@ -142,26 +144,24 @@ export class Future extends Core { // near :: Promise e a -> Promise e a near () { - if (!this._isResolved()) { + if (!this._isResolved() || this.ref === this) { return this + } else { + this.ref = this.ref.near() + return this.ref } - - this.ref = this.ref.near() - return this.ref } // state :: Promise e a -> Int state () { - return this._isResolved() ? this.ref.near().state() : PENDING + return this._isResolved() && this.ref !== this ? this.ref.near().state() : PENDING } _isResolved () { - if (this.ref !== void 0) return true if (this.token != null && this.token.requested) { - this.__become(this.token.getRejected()) - return true + this.__become(this.token.getCancelled()) } - return false + return this.ref !== void 0 } _when (action) { @@ -176,8 +176,42 @@ export class Future extends Core { } } - _resolve (x) { - this._become(resolve(x, this.token)) + _resolve (x, cancelAction) { + if (this._isResolved()) { + return // TODO: still resolve thenables when cancelled? + } + if (isPromise(x)) { + this._resolvePromise(x.near(), cancelAction) + } else { + // TODO: can a thenable end up with a Never? + if (cancelAction) { + cancelAction.end() + } + this.__become(isObject(x) ? refForMaybeThenable(x, this.token) : new Fulfilled(x)) + } + } + + _resolvePromise (p, cancelAction) { + /* eslint complexity:[2,6] */ + if (p === this) { + p = cycle() + } else { + const state = p.state() + if ((state & NEVER) > 0) { + p = p.untilCancel(this.token) + } else if ((state & CANCELLED) > 0) { + p = reject(p.value) + } else if ((state & PENDING) > 0 && this.token !== p.token) { + this.ref = this + // reuse cancelAction - do not .end() it here + p._runAction(cancelAction || new Action(this)) + return + } + } + if (cancelAction) { + cancelAction.end() + } + this.__become(p) } _fulfill (x) { @@ -189,7 +223,7 @@ export class Future extends Core { return } - this.__become(new Rejected(e)) + this.__become(reject(e)) } _become (p) { @@ -201,7 +235,10 @@ export class Future extends Core { } __become (p) { - this.ref = p === this ? cycle() : p + // assert: isSettled(p) || isNever(p) || p.token === this.token + // assert: this.ref == null || this.ref === this + this.ref = p + this.token = null if (this.action === void 0) { return @@ -235,11 +272,11 @@ class Fulfilled extends Core { } then (f, _, token) { - return typeof f === 'function' ? then(f, void 0, this, new Future(token)) : rejectedIfCancelled(token, this) + return typeof f === 'function' ? then(f, void 0, this, new Future(token)) : cancelledIfRequested(token, this) } catch (_, token) { - return rejectedIfCancelled(token, this) + return cancelledIfRequested(token, this) } map (f, token) { @@ -259,7 +296,7 @@ class Fulfilled extends Core { } untilCancel (token) { - return rejectedIfCancelled(token, this) + return cancelledIfRequested(token, this) } finally (f) { @@ -270,10 +307,6 @@ class Fulfilled extends Core { return typeof f === 'function' ? then(f, undefined, this, new Future()) : this } - toString () { - return '[object ' + this.inspect() + ']' - } - inspect () { return 'Promise { fulfilled: ' + this.value + ' }' } @@ -306,7 +339,7 @@ class Rejected extends Core { } then (_, r, token) { - return typeof r === 'function' ? this.catch(r, token) : rejectedIfCancelled(token, this) + return typeof r === 'function' ? this.catch(r, token) : this._cancelledIfRequested(token) } catch (r, token) { @@ -314,23 +347,23 @@ class Rejected extends Core { } map (_, token) { - return rejectedIfCancelled(token, this) + return this._cancelledIfRequested(token) } ap (_, token) { - return rejectedIfCancelled(token, this) + return this._cancelledIfRequested(token) } chain (_, token) { - return rejectedIfCancelled(token, this) + return this._cancelledIfRequested(token) } concat (_) { - return this + return this._cancelledIfRequested(null) } untilCancel (token) { - return rejectedIfCancelled(token, this) + return this._cancelledIfRequested(token) } finally (f) { @@ -338,11 +371,7 @@ class Rejected extends Core { } trifurcate (f, r, c) { - return this.token ? trifurcate(undefined, r, c, this, new Future()) : typeof r === 'function' ? then(undefined, r, this, new Future()) : this - } - - toString () { - return '[object ' + this.inspect() + ']' + return typeof r === 'function' ? then(undefined, r, this, new Future()) : this } inspect () { @@ -357,6 +386,10 @@ class Rejected extends Core { return this } + _cancelledIfRequested (token) { + return cancelledIfRequested(token, this) + } + _when (action) { taskQueue.add(new Continuation(action, this)) } @@ -369,27 +402,54 @@ class Rejected extends Core { } } +// Cancelled :: Error e => e -> Promise e a +// A promise whose value was invalidated and cannot be known +class Cancelled extends Rejected { + trifurcate (f, r, c) { + return trifurcate(undefined, undefined, c, this, new Future()) + } + + inspect () { + return 'Promise { cancelled: ' + this.value + ' }' + } + + state () { + return REJECTED | CANCELLED | HANDLED + } + + _cancelledIfRequested (token) { + // like cancelledIfRequested(token, this), but not quite + token = CancelToken.from(token) + return token != null && token.requested ? token.getCancelled() : reject(this.value) + } + + _runAction (action) { + // assert: action.promise != null + action.cancelled(this) + } +} + // Never :: Promise e a // A promise that waits forever for its value to be known class Never extends Core { then (_, __, token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } catch (_, token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } map (_, token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } ap (_, token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } chain (_, token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } concat (b) { @@ -397,7 +457,7 @@ class Never extends Core { } untilCancel (token) { - return rejectedWhenCancel(token, this) + return cancelledWhen(token, this) } finally (_) { @@ -432,7 +492,8 @@ class Never extends Core { } const silencer = new Action(never()) -silencer.fulfilled = function fulfilled (p) { } +silencer.fulfilled = noop +silencer.cancelled = noop silencer.rejected = function setHandled (p) { p._state |= HANDLED } @@ -441,11 +502,6 @@ export function silenceError (p) { p._runAction(silencer) } -export function silentReject (e) { - const r = new Rejected(e) - r._state |= HANDLED - return r -} // ------------------------------------------------------------- // ## Creating promises // ------------------------------------------------------------- @@ -454,11 +510,11 @@ export function silentReject (e) { // resolve :: Thenable e a -> Promise e a // resolve :: a -> Promise e a export function resolve (x, token) { - /* eslint complexity:[2,6] */ + /* eslint complexity:[2,7] */ if (isPromise(x)) { return x.untilCancel(token) } else if (token != null && token.requested) { - return token.getRejected() + return token.getCancelled() } else if (isObject(x)) { return refForMaybeThenable(x, token) } else { @@ -477,6 +533,11 @@ export function reject (e) { return r } +// cancel :: e -> Promise e a +export function cancel (e) { + return new Cancelled(e) +} + // never :: Promise e a export function never () { return new Never() @@ -491,7 +552,50 @@ export function fulfill (x) { // type Resolve e a = a|Thenable e a -> () export function future (token) { const promise = new Future(token) - return {resolve: x => promise._resolve(x), promise} + if (promise.token == null) { + return { + promise, + resolve (x) { promise._resolve(x) } + } + } + let put = new Action(promise) + return { + promise, + resolve (x) { + if (put == null) return + promise._resolve(x, put) + put = null + } + } +} + +// makeResolvers :: Promise e a -> { resolve: Resolve e a, reject: e -> () } +export function makeResolvers (promise) { + if (promise.token != null) { + let put = new Action(promise) + return { + resolve (x) { + if (put == null || put.promise == null) return + promise._resolve(x, put) + put = promise = null + }, + reject (e) { + if (put == null || put.promise == null) return + promise._reject(e) + put.end() + put = promise = null + } + } + } else { + return { + resolve (x) { + promise._resolve(x) + }, + reject (e) { + promise._reject(e) + } + } + } } // ------------------------------------------------------------- @@ -503,34 +607,32 @@ function isPromise (x) { return x instanceof Core } -function rejectedIfCancelled (token, settled) { - if (token == null) return settled +function cancelledIfRequested (token, settled) { token = CancelToken.from(token) - if (token.requested) return token.getRejected() - return settled + return token != null && token.requested ? token.getCancelled() : settled } -function rejectedWhenCancel (token, never) { + +function cancelledWhen (token, never) { if (token == null) return never - return CancelToken.from(token).getRejected() + return CancelToken.from(token).getCancelled() } function refForMaybeThenable (x, token) { try { const then = x.then return typeof then === 'function' - ? extractThenable(then, x, token) + ? extractThenable(then, x, new Future(token)) : fulfill(x) } catch (e) { - return new Rejected(e) + return reject(e) } } // WARNING: Naming the first arg "then" triggers babel compilation bug -function extractThenable (thn, thenable, token) { - const p = new Future() - +function extractThenable (thn, thenable, p) { + const { resolve, reject } = makeResolvers(p) try { - thn.call(thenable, x => p._resolve(x), e => p._reject(e), token) + thn.call(thenable, resolve, reject, p.token) } catch (e) { p._reject(e) } @@ -539,5 +641,5 @@ function extractThenable (thn, thenable, token) { } function cycle () { - return new Rejected(new TypeError('resolution cycle')) + return reject(new TypeError('resolution cycle')) } diff --git a/src/chain.js b/src/chain.js index 21ded18..60b97ad 100644 --- a/src/chain.js +++ b/src/chain.js @@ -1,34 +1,20 @@ import { isObject } from './util' -import Action from './Action' +import { CancellableAction } from './Action' export default function chain (f, p, promise) { if (promise.token != null && promise.token.requested) { - return promise.token.getRejected() + return promise.token.getCancelled() } p._when(new Chain(f, promise)) return promise } -class Chain extends Action { - constructor (f, promise) { - super(promise) - this.f = f - } - - destroy () { - super.destroy() - this.f = null - } - - fulfilled (p) { - if (this.tryCall(this.f, p.value)) this.tryUnsubscribe() - } - +class Chain extends CancellableAction { handle (y) { if (!(isObject(y) && typeof y.then === 'function')) { - this.promise._reject(new TypeError('f must return a promise')) + this.end()._reject(new TypeError('f must return a promise')) + } else { + this.promise._resolve(y, this) } - - this.promise._resolve(y) } } diff --git a/src/combinators.js b/src/combinators.js index 859a10a..556a639 100644 --- a/src/combinators.js +++ b/src/combinators.js @@ -67,6 +67,7 @@ class MergeHandler { run () { try { + // assert: this.promise.token == null 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 e3a2aeb..bb6ffdc 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -61,7 +61,7 @@ class Coroutine extends Action { cancel (p) { /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself - // assert: p === this.promise.token.getRejected() + // assert: p === this.promise.token.getCancelled() this.promise = null const res = new Future() this.generator = new Coroutine(this.generator, res, p.near().value) @@ -84,7 +84,7 @@ class Coroutine extends Action { stack.pop() // assert: === this } if (this.promise) { - const res = resolve(result.value, this.promise.token) + const res = resolve(result.value, this.promise.token) // TODO optimise token? if (result.done) { this.put(res) } else { @@ -102,7 +102,7 @@ class Coroutine extends Action { this.generator = null const reason = cancelRoutine.tokenref cancelRoutine.tokenref = null // not cancellable - // assert: reason === this.tokenref.get().getRejected().value + // assert: reason === this.tokenref.get().getCancelled().value this.tokenref = null cancelRoutine.step(cancelRoutine.generator.return, reason) } diff --git a/src/finally.js b/src/finally.js index 2aef39d..0bdd513 100644 --- a/src/finally.js +++ b/src/finally.js @@ -1,6 +1,6 @@ import { resolve } from './Promise' import { isRejected, isFulfilled } from './inspect' -import Action from './Action' +import { CancellableAction } from './Action' export default function _finally (f, p, promise) { // assert: promise.token == null @@ -9,14 +9,13 @@ export default function _finally (f, p, promise) { return promise } -class Final extends Action { +class Final extends CancellableAction { constructor (f, t, promise) { - super(promise) + super(f, promise) this.token = t if (t != null) { t._subscribe(this) } - this.f = f } /* istanbul ignore next */ @@ -28,39 +27,51 @@ class Final extends Action { /* istanbul ignore if */ if (this.token == null) return this.token = null - return this.tryFin(p) + const promise = this.tryFin(p) + this.promise = null // prevent cancelled from running + return promise } fulfilled (p) { - this.tryFin(p) + this.settled(p, this.f) } rejected (p) { - this.tryFin(p) - return true // TODO: correctness? track again afterwards? + return this.settled(p, p) + } + + settled (p, res) { + if (typeof this.f === 'function') { // f is the callback + const token = this.token + if (token) { + token._unsubscribe(this) + this.token = null + } + this.tryFin(p) + return true + } else { // f held the original result + this.promise.__become(res) + this.promise = this.f = null + return false + } } tryFin (p) { /* eslint complexity:[2,5] */ - const f = this.f - if (typeof f !== 'function') return this.promise - this.f = null - const token = this.token - if (token) { - token._unsubscribe(this) - this.token = null - } const orig = this.promise - if (!this.tryCall(f, p)) { + if (!this.tryCall(this.f, p)) { // assert: orig !== this.promise // assert: !isRejeced(this.promise) if (isFulfilled(this.promise)) { orig._become(p) } else { - this.promise._runAction(new Put(p, orig)) + this.f = p + this.promise._runAction(this) + this.promise = orig } + return this.promise } - return this.promise + return orig } handle (result) { @@ -71,15 +82,8 @@ class Final extends Action { this.promise = p } } -} -class Put extends Action { - constructor (promise, target) { - super(target) - this.p = promise - } - - fulfilled (_) { - this.put(this.p) + end () { + return this.promise } } diff --git a/src/inspect.js b/src/inspect.js index 5eaad63..f6c38b1 100644 --- a/src/inspect.js +++ b/src/inspect.js @@ -1,4 +1,4 @@ -import { PENDING, FULFILLED, REJECTED, SETTLED, NEVER, HANDLED } from './state' +import { PENDING, FULFILLED, REJECTED, CANCELLED, SETTLED, NEVER, HANDLED } from './state' import { silenceError } from './Promise' // deferred export function isPending (p) { @@ -13,6 +13,10 @@ export function isRejected (p) { return (p.state() & REJECTED) > 0 } +export function isCancelled (p) { + return (p.state() & CANCELLED) > 0 +} + export function isSettled (p) { return (p.state() & SETTLED) > 0 } diff --git a/src/main.js b/src/main.js index 62788ab..1e901cf 100644 --- a/src/main.js +++ b/src/main.js @@ -4,8 +4,8 @@ /* 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 { Future, resolve, reject, makeResolvers } from './Promise' +export { isFulfilled, isRejected, isCancelled, 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' @@ -14,8 +14,6 @@ export { default as CancelToken } from './CancelToken' export { default as coroutine } from './coroutine.js' -import Action from './Action' - import _delay from './delay' import _timeout from './timeout' @@ -32,17 +30,30 @@ import _runNode from './node' // fromNode :: NodeApi e a -> (...args -> Promise e a) // Turn a Node API into a promise API export function fromNode (f) { + checkFunction(f) return function promisified (...args) { - return runResolver(_runNode, f, this, args, new Future()) + return runNodeFunction(f, this, args) } } // runNode :: NodeApi e a -> ...* -> Promise e a // Run a Node API, returning a promise for the outcome export function runNode (f, ...args) { - return runResolver(_runNode, f, this, args, new Future()) + checkFunction(f) + return runNodeFunction(f, this, args) } +function runNodeFunction (f, thisArg, args) { + const p = new Future() + + try { + _runNode(f, thisArg, args, p) + } catch (e) { + p._reject(e) + } + + return p +} // ------------------------------------------------------------- // ## Make a promise // ------------------------------------------------------------- @@ -52,16 +63,17 @@ export function runNode (f, ...args) { // type Producer e a = (...* -> Resolve e a -> Reject e -> ()) // runPromise :: Producer e a -> ...* -> Promise e a export function runPromise (f, ...args) { - return runResolver(_runPromise, f, this, args, new Future()) + checkFunction(f) + return runResolver(f, this, args, new Future()) } -function runResolver (run, f, thisArg, args, p) { - checkFunction(f) +function runResolver (f, thisArg, args, p) { + const resolvers = makeResolvers(p) try { - run(f, thisArg, args, p) + _runPromise(f, thisArg, args, resolvers) } catch (e) { - p._reject(e) + resolvers.reject(e) } return p @@ -80,7 +92,7 @@ function checkFunction (f) { // delay :: number -> Promise e a -> Promise e a export function delay (ms, x, token) { /* eslint complexity:[2,5] */ - if (token != null && token.requested) return token.getRejected() + if (token != null && token.requested) return token.getCancelled() const p = resolve(x) if (ms <= 0) return p if (token == null && (isRejected(p) || isNever(p))) return p @@ -105,22 +117,9 @@ const NOARGS = [] class CreedPromise extends Future { constructor (f, token) { super(token) - if (this.token != null) { - if (this.token.requested) { - this._resolve(this.token.getRejected()) - return - } - this.cancelAction = new Action(this) - this.token._subscribe(this.cancelAction) - } - runResolver(_runPromise, f, void 0, NOARGS, this) - } - __become (p) { - if (this.token != null && this.cancelAction != null) { - this.token._unsubscribe(this.cancelAction) // TODO better solution - this.cancelAction = null + if (!this._isResolved()) { // test for cancellation + runResolver(f, void 0, NOARGS, this) } - super.__become(p) } } diff --git a/src/map.js b/src/map.js index 4038380..5640ade 100644 --- a/src/map.js +++ b/src/map.js @@ -1,28 +1,14 @@ -import Action from './Action' +import { CancellableAction } from './Action' export default function map (f, p, promise) { if (promise.token != null && promise.token.requested) { - return promise.token.getRejected() + return promise.token.getCancelled() } p._when(new Map(f, promise)) return promise } -class Map extends Action { - constructor (f, promise) { - super(promise) - this.f = f - } - - destroy () { - super.destroy() - this.f = null - } - - fulfilled (p) { - if (this.tryCall(this.f, p.value)) this.tryUnsubscribe() - } - +class Map extends CancellableAction { handle (result) { this.promise._fulfill(result) } diff --git a/src/runPromise.js b/src/runPromise.js index b6b966c..7fae73a 100644 --- a/src/runPromise.js +++ b/src/runPromise.js @@ -1,12 +1,6 @@ -export default function runPromise (f, thisArg, args, promise) { +export default function runPromise (f, thisArg, args, resolvers) { /* eslint complexity:[2,5] */ - function resolve (x) { - promise._resolve(x) - } - - function reject (e) { - promise._reject(e) - } + const { resolve, reject } = resolvers switch (args.length) { case 0: @@ -25,6 +19,4 @@ export default function runPromise (f, thisArg, args, promise) { args.push(resolve, reject) f.apply(thisArg, args) } - - return promise } diff --git a/src/state.js b/src/state.js index b932693..c84bfef 100644 --- a/src/state.js +++ b/src/state.js @@ -4,5 +4,6 @@ export const FULFILLED = 1 << 1 export const REJECTED = 1 << 2 export const SETTLED = FULFILLED | REJECTED export const NEVER = 1 << 3 +export const CANCELLED = 1 << 4 -export const HANDLED = 1 << 4 +export const HANDLED = 1 << 5 diff --git a/src/subscribe.js b/src/subscribe.js index 41c501d..b39c984 100644 --- a/src/subscribe.js +++ b/src/subscribe.js @@ -1,8 +1,9 @@ -import Action from './Action' +import { noop } from './util' +import { CancellableAction } from './Action' export function subscribe (f, t, promise) { if (promise.token != null && promise.token.requested) { - return promise.token.getRejected() + return promise.token.getCancelled() } t._subscribe(new Subscription(f, promise)) return promise @@ -14,7 +15,7 @@ export function subscribeOrCall (f, g, t, promise) { return function call () { // TODO: should `g` run despite `t.requested`, // or should none run immediately despite `call` having been called? - if (sub != null && sub.f != null) { + if (sub != null && sub.f != null && sub.f !== noop) { // noop is the "currently running" sentinel t._unsubscribe(sub) t = sub = null if (typeof g === 'function') { @@ -24,17 +25,7 @@ export function subscribeOrCall (f, g, t, promise) { } } -class Subscription extends Action { - constructor (f, promise) { - super(promise) - this.f = f - } - - destroy () { - super.destroy() - this.f = null - } - +class Subscription extends CancellableAction { cancel (p) { /* eslint complexity:[2,4] */ const promise = this.promise @@ -44,13 +35,7 @@ class Subscription extends Action { return res } } - const f = this.f - this.f = null - if (this.tryCall(f, p.near().value)) this.tryUnsubscribe() + this.tryCall(this.f, p.near().value) return promise } - - handle (result) { - this.promise._resolve(result) - } } diff --git a/src/then.js b/src/then.js index 6d8e15c..b7a47c6 100644 --- a/src/then.js +++ b/src/then.js @@ -1,23 +1,21 @@ -import Action from './Action' +import { CancellableAction } from './Action' export default function then (f, r, p, promise) { if (promise.token != null && promise.token.requested) { - return promise.token.getRejected() + return promise.token.getCancelled() } p._when(new Then(f, r, promise)) return promise } -class Then extends Action { +class Then extends CancellableAction { constructor (f, r, promise) { - super(promise) - this.f = f + super(f, promise) this.r = r } destroy () { super.destroy() - this.f = null this.r = null } @@ -30,16 +28,13 @@ class Then extends Action { } runThen (f, p) { - const hasHandler = typeof f === 'function' + const hasHandler = (this.f != null || this.r != null) && typeof f === 'function' if (hasHandler) { - if (this.tryCall(f, p.value)) this.tryUnsubscribe() + this.r = null + this.tryCall(f, p.value) } else { this.put(p) } return hasHandler } - - handle (result) { - this.promise._resolve(result) - } } diff --git a/src/trifurcate.js b/src/trifurcate.js index a3749f3..9a526ea 100644 --- a/src/trifurcate.js +++ b/src/trifurcate.js @@ -1,59 +1,50 @@ -import Action from './Action' +import { CancellableAction } from './Action' export default function trifurcate (f, r, c, p, promise) { // assert: promise.token == null - // assert: p.token != null - p._when(new Trifurcation(f, r, c, p.token, promise)) + p._when(new Trifurcation(f, r, c, promise)) return promise } -class Trifurcation extends Action { - constructor (f, r, c, t, promise) { - super(promise) - this.token = t - t._subscribe(this) - this.f = f +class Trifurcation extends CancellableAction { + constructor (f, r, c, promise) { + super(f, promise) this.r = r this.c = c } - /* istanbul ignore next */ - destroy () { // possibly called when unsubscribed from the token - this.token = null - } - - cancel (p) { - /* istanbul ignore if */ - if (this.token == null) return - this.runTee(this.c, p.near()) - } - fulfilled (p) { - this.token._unsubscribe(this) this.runTee(this.f, p) } rejected (p) { - this.token._unsubscribe(this) return this.runTee(this.r, p) } + cancelled (p) { + if (typeof this.c !== 'function') { + this.end()._reject(p.value) + } else { + this.runTee(this.c, p) + } + } + runTee (f, p) { - this.token = null - this.f = null - this.r = null - this.c = null - const hasHandler = typeof f === 'function' + /* eslint complexity:[2,4] */ + const hasHandler = (this.f != null || this.r != null || this.c != null) && typeof f === 'function' if (hasHandler) { + this.r = null + this.c = null this.tryCall(f, p.value) } else { this.put(p) } - this.promise = null return hasHandler } - handle (result) { - this.promise._resolve(result) + end () { + const promise = this.promise + this.promise = null + return promise } } diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index 24b7935..120956d 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { CancelToken, isRejected, isPending, getReason, future } from '../src/main' +import { CancelToken, isCancelled, isPending, getReason, future } from '../src/main' import { assertSame, FakeCancelAction } from './lib/test-util' import assert from 'assert' @@ -38,43 +38,43 @@ describe('CancelToken', function () { assert.strictEqual(token.requested, true) }) - describe('getRejected()', () => { - it('should return a rejected promise after the token was cancelled', () => { + describe('getCancelled()', () => { + it('should return a cancelled promise after the token was cancelled', () => { const {token, cancel} = CancelToken.source() cancel() - assert(isRejected(token.getRejected())) + assert(isCancelled(token.getCancelled())) }) it('should return a pending promise until the token is cancelled', () => { const {token, cancel} = CancelToken.source() - const p = token.getRejected() + const p = token.getCancelled() assert(isPending(p)) cancel() - assert(isRejected(p)) + assert(isCancelled(p)) }) - it('should reject with the argument of the first cancel call', () => { + it('should cancel with the argument of the first cancel call', () => { const {token, cancel} = CancelToken.source() const r = {} cancel(r) cancel({}) - const p = token.getRejected() + const p = token.getCancelled() cancel({}) - return p.then(assert.ifError, e => { + return p.trifurcate(assert.ifError, assert.ifError, e => { assert.strictEqual(e, r) }) }) it('should return a pending promise until the token is asynchronously cancelled', () => { const {token, cancel} = CancelToken.source() - const p = token.getRejected() + const p = token.getCancelled() const r = {} setTimeout(() => { assert(isPending(p)) assert(!token.requested) cancel(r) }, 5) - return p.then(assert.ifError, e => { + return p.trifurcate(assert.ifError, assert.ifError, e => { assert(token.requested) assert.strictEqual(e, r) }) @@ -248,10 +248,10 @@ describe('CancelToken', function () { }) }) - it('should behave nearly like getRejected().catch()', () => { + it('should behave nearly like getCancelled().catch()', () => { const {token, cancel} = CancelToken.source() const p = token.subscribe(x => x) - const q = token.getRejected().catch(x => x) + const q = token.getCancelled().catch(x => x) const res = cancel({}) assert.strictEqual(res.length, 1) assert.strictEqual(res[0], p) @@ -290,14 +290,14 @@ describe('CancelToken', function () { assert(!called) a.cancel() assert(!called) - return assertSame(p, b.token.getRejected()) + return assertSame(p, b.token.getCancelled()) }) it('should return cancellation with already cancelled token', () => { const {token, cancel} = CancelToken.source() cancel() const p = CancelToken.empty().subscribe(() => {}, token) - assert.strictEqual(p, token.getRejected()) + assert.strictEqual(p, token.getCancelled()) }) it('should behave like cancellation when token is cancelled from the subscription', () => { @@ -308,7 +308,7 @@ describe('CancelToken', function () { }, b.token) assert.strictEqual(p.token, b.token) a.cancel() - return assertSame(p, b.token.getRejected()) + return assertSame(p, b.token.getCancelled()) }) it('should call subscriptions that are subscribed from the callback', () => { @@ -411,7 +411,7 @@ describe('CancelToken', function () { a.cancel(r) assert(token.requested) b.cancel({}) - return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r)) }) it('should cancel the result token when b is cancelled first', () => { @@ -423,7 +423,7 @@ describe('CancelToken', function () { b.cancel(r) assert(token.requested) a.cancel({}) - return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r)) }) it('should cancel the result token when a is already cancelled', () => { @@ -434,7 +434,7 @@ describe('CancelToken', function () { const token = a.token.concat(b.token) assert(token.requested) b.cancel({}) - return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r)) }) it('should cancel the result token when b is already cancelled', () => { @@ -445,7 +445,7 @@ describe('CancelToken', function () { const token = a.token.concat(b.token) assert(token.requested) a.cancel({}) - return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r)) }) }) @@ -466,7 +466,7 @@ describe('CancelToken', function () { assert(!token.requested) const r = {} resolve(r) - return token.getRejected().then(assert.ifError, e => assert.strictEqual(e, r)) + return token.getCancelled().then(assert.ifError, e => assert.strictEqual(e, r)) }) }) @@ -483,7 +483,7 @@ describe('CancelToken', function () { b.cancel() race.add(b.token) assert(race.get().requested) - return race.get().getRejected().then(assert.ifError, e => { + return race.get().getCancelled().then(assert.ifError, e => { assert.strictEqual(e, expected) const c = CancelToken.source() race.add(c.token) @@ -525,7 +525,7 @@ describe('CancelToken', function () { assert(!pool.get().requested) sources[i].cancel(reasons[i]) } - return pool.get().getRejected().then(assert.ifError, r => { + return pool.get().getCancelled().then(assert.ifError, r => { assert.deepEqual(r, reasons) }) }) @@ -566,7 +566,7 @@ describe('CancelToken', function () { s.cancel() } } - return pool.get().getRejected().then(assert.ifError, r => { + return pool.get().getCancelled().then(assert.ifError, r => { assert(token.requested) }) }) @@ -582,7 +582,7 @@ describe('CancelToken', function () { pool.add(c.token) c.cancel() a.cancel() - return pool.get().getRejected().then(assert.ifError, () => { + return pool.get().getCancelled().then(assert.ifError, () => { const d = CancelToken.source() pool.add(d.token) assert(pool.get().requested) @@ -618,7 +618,7 @@ describe('CancelToken', function () { assert(CancelToken.pool([token]).get().requested) const pool = CancelToken.pool() pool.add(token) - return pool.get().getRejected().then(assert.ifError, r => { + return pool.get().getCancelled().then(assert.ifError, r => { assert.strictEqual(r.length, 1) assert.strictEqual(r[0], expected) }) @@ -638,7 +638,7 @@ describe('CancelToken', function () { assert(!ref.get().requested) cancel({}) assert(ref.get().requested) - return assertSame(token.getRejected(), ref.get().getRejected()) + return assertSame(token.getCancelled(), ref.get().getCancelled()) }) it('should throw when assigned to after cancellation', () => { diff --git a/test/Promise-test.js b/test/Promise-test.js index 4ac7bd9..1b8c009 100644 --- a/test/Promise-test.js +++ b/test/Promise-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { Promise, fulfill, reject, isRejected, CancelToken } from '../src/main' +import { Promise, fulfill, reject, isCancelled, CancelToken } from '../src/main' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -14,16 +14,16 @@ describe('Promise', () => { return p }) - it('should not call executor and immediately reject when token is requested', () => { + it('should not call executor and immediately cancel when token is requested', () => { const {token, cancel} = CancelToken.source() cancel({}) let called = false const p = new Promise((resolve, reject) => { called = true }, token) - assert(isRejected(p)) + assert(isCancelled(p)) assert(!called) - return assertSame(token.getRejected(), p) + return assertSame(token.getCancelled(), p) }) it('should reject if resolver throws synchronously', () => { @@ -111,12 +111,12 @@ describe('Promise', () => { }) describe('token', () => { - it('should immediately reject the promise when cancelled', () => { + it('should immediately cancel the promise when cancelled', () => { const {token, cancel} = CancelToken.source() const expected = new Error() const p = new Promise(resolve => {}, token) cancel(expected) - assert(isRejected(p)) + assert(isCancelled(p)) return p.then(assert.ifError, x => assert.strictEqual(expected, x)) }) diff --git a/test/cancellation-test.js b/test/cancellation-test.js index f423a13..e54a747 100644 --- a/test/cancellation-test.js +++ b/test/cancellation-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { future, reject, fulfill, never, delay, isRejected, CancelToken } from '../src/main' +import { future, reject, fulfill, never, delay, isCancelled, CancelToken } from '../src/main' import { silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -20,7 +20,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).then(assert.ifError, null, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -29,7 +29,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).catch(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) */ @@ -38,7 +38,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).map(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -47,7 +47,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(assert.ifError).ap(fulfill(1), token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -56,7 +56,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).chain(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -67,7 +67,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() fulfill(1).then(() => cancel({})) const res = fulfill(1).then(assert.ifError, null, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -76,7 +76,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() fulfill(1).then(() => cancel({})) const res = fulfill(1).catch(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) */ @@ -85,7 +85,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() fulfill(1).then(() => cancel({})) const res = fulfill(1).map(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -94,7 +94,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() fulfill(1).then(() => cancel({})) const res = fulfill(assert.ifError).ap(fulfill(1), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -103,7 +103,7 @@ describe('fulfill', () => { const { token, cancel } = CancelToken.source() fulfill(1).then(() => cancel({})) const res = fulfill(1).chain(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -116,13 +116,13 @@ describe('fulfill', () => { cancel({}) throw new Error() }, null, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).then(() => cancel({}), null, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -130,7 +130,7 @@ describe('fulfill', () => { it('should behave like cancellation for fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).map(() => cancel({}), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -138,7 +138,7 @@ describe('fulfill', () => { it('should behave like cancellation for fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(() => cancel({})).ap(fulfill(1), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -146,7 +146,7 @@ describe('fulfill', () => { it('should behave like cancellation for fulfill', () => { const { token, cancel } = CancelToken.source() const res = fulfill(1).chain(() => cancel({}), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -236,7 +236,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).catch(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -245,7 +245,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).then(assert.ifError, null, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -254,7 +254,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).map(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -263,7 +263,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).ap(fulfill(1), token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -272,7 +272,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).chain(assert.ifError, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) */ }) @@ -283,7 +283,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() reject(1).catch(() => cancel({})) const res = silenced(reject(1)).catch(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -292,7 +292,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() reject(1).catch(() => cancel({})) const res = silenced(reject(1)).then(assert.ifError, null, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -301,7 +301,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() reject(1).catch(() => cancel({})) const res = silenced(reject(1)).map(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -310,7 +310,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() reject(1).catch(() => cancel({})) const res = silenced(reject(1)).ap(fulfill(1), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -319,7 +319,7 @@ describe('reject', () => { const { token, cancel } = CancelToken.source() reject(1).catch(() => cancel({})) const res = silenced(reject(1)).chain(assert.ifError, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) */ }) @@ -332,13 +332,13 @@ describe('reject', () => { cancel({}) throw new Error() }, token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { const { token, cancel } = CancelToken.source() const res = silenced(reject(1)).catch(() => cancel({}), token) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -428,7 +428,7 @@ describe('future', () => { const a = future(token) const b = future() a.resolve(b.promise) - const res = assertSame(a.promise, token.getRejected()) + const res = assertSame(a.promise, token.getCancelled()) cancel({}) return res }) @@ -439,7 +439,7 @@ describe('future', () => { const b = future() a.resolve(b.promise) cancel({}) - return assertSame(a.promise, token.getRejected()) + return assertSame(a.promise, token.getCancelled()) }) it('should behave like cancellation before cancelled for same-token resolution', () => { @@ -447,7 +447,7 @@ describe('future', () => { const a = future(token) const b = future(token) a.resolve(b.promise) - const res = assertSame(a.promise, token.getRejected()) + const res = assertSame(a.promise, token.getCancelled()) cancel({}) return res }) @@ -458,7 +458,7 @@ describe('future', () => { const b = future(token) a.resolve(b.promise) cancel({}) - return assertSame(a.promise, token.getRejected()) + return assertSame(a.promise, token.getCancelled()) }) it('should behave like cancellation before cancelled for different-token resolution', () => { @@ -466,7 +466,7 @@ describe('future', () => { const a = future(token) const b = future(CancelToken.empty()) a.resolve(b.promise) - const res = assertSame(a.promise, token.getRejected()) + const res = assertSame(a.promise, token.getCancelled()) cancel({}) return res }) @@ -477,7 +477,7 @@ describe('future', () => { const b = future(CancelToken.empty()) a.resolve(b.promise) cancel({}) - return assertSame(a.promise, token.getRejected()) + return assertSame(a.promise, token.getCancelled()) }) it('should behave like rejection before resolution cancelled for no token', () => { @@ -685,7 +685,7 @@ describe('future', () => { const { promise } = future() const res = promise.then(null, null, token) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation when cancelled for never', () => { @@ -694,7 +694,7 @@ describe('future', () => { const res = promise.then(null, null, token) resolve(never()) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like fulfillment when never cancelled for fulfill', () => { @@ -872,7 +872,7 @@ describe('future', () => { cancel({}) const res = promise.then(assert.ifError, null, token) resolve(fulfill(1)) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) it('should return cancellation for reject', () => { @@ -881,7 +881,7 @@ describe('future', () => { cancel({}) const res = promise.then(assert.ifError, null, token) resolve(silenced(reject(1))) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) }) @@ -892,7 +892,7 @@ describe('future', () => { cancel({}) const res = promise.catch(assert.ifError, token) resolve(fulfill(1)) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) it('should return cancellation for reject', () => { @@ -901,7 +901,7 @@ describe('future', () => { cancel({}) const res = promise.catch(assert.ifError, token) resolve(silenced(reject(1))) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) }) @@ -912,7 +912,7 @@ describe('future', () => { cancel({}) const res = promise.map(assert.ifError, token) resolve(fulfill(1)) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) it('should return cancellation for reject', () => { @@ -921,7 +921,7 @@ describe('future', () => { cancel({}) const res = promise.map(assert.ifError, token) resolve(silenced(reject(1))) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) }) @@ -932,7 +932,7 @@ describe('future', () => { cancel({}) const res = promise.ap(fulfill(1), token) resolve(fulfill(assert.ifError)) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) it('should return cancellation for reject', () => { @@ -941,7 +941,7 @@ describe('future', () => { cancel({}) const res = promise.ap(fulfill(1), token) resolve(silenced(reject(1))) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) }) @@ -952,7 +952,7 @@ describe('future', () => { cancel({}) const res = promise.chain(assert.ifError, token) resolve(fulfill(1)) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) it('should return cancellation for reject', () => { @@ -961,7 +961,7 @@ describe('future', () => { cancel({}) const res = promise.chain(assert.ifError, token) resolve(silenced(reject(1))) - assert.strictEqual(token.getRejected(), res) + assert.strictEqual(token.getCancelled(), res) }) }) }) @@ -973,8 +973,8 @@ describe('future', () => { const { promise } = future() const res = promise.then(assert.ifError, null, token) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for never', () => { @@ -983,8 +983,8 @@ describe('future', () => { const res = promise.then(assert.ifError, null, token) resolve(never()) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) }) @@ -994,8 +994,8 @@ describe('future', () => { const { promise } = future() const res = promise.catch(assert.ifError, token) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for never', () => { @@ -1004,8 +1004,8 @@ describe('future', () => { const res = promise.catch(assert.ifError, token) resolve(never()) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) }) @@ -1015,8 +1015,8 @@ describe('future', () => { const { promise } = future() const res = promise.map(assert.ifError, token) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for never', () => { @@ -1025,8 +1025,8 @@ describe('future', () => { const res = promise.map(assert.ifError, token) resolve(never()) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) }) @@ -1036,8 +1036,8 @@ describe('future', () => { const { promise } = future() const res = promise.ap(fulfill(1), token) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for never', () => { @@ -1046,8 +1046,8 @@ describe('future', () => { const res = promise.ap(fulfill(1), token) resolve(never()) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) }) @@ -1057,8 +1057,8 @@ describe('future', () => { const { promise } = future() const res = promise.chain(assert.ifError, token) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for never', () => { @@ -1067,8 +1067,8 @@ describe('future', () => { const res = promise.chain(assert.ifError, token) resolve(never()) cancel({}) - assert(isRejected(res)) - return assertSame(token.getRejected(), res) + assert(isCancelled(res)) + return assertSame(token.getCancelled(), res) }) }) }) @@ -1081,7 +1081,7 @@ describe('future', () => { const res = promise.then(assert.ifError, null, token) cancel({}) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1090,7 +1090,7 @@ describe('future', () => { const res = promise.then(assert.ifError, null, token) cancel({}) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1101,7 +1101,7 @@ describe('future', () => { const res = promise.catch(assert.ifError, token) cancel({}) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1110,7 +1110,7 @@ describe('future', () => { const res = promise.catch(assert.ifError, token) cancel({}) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1121,7 +1121,7 @@ describe('future', () => { const res = promise.map(assert.ifError, token) cancel({}) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1130,7 +1130,7 @@ describe('future', () => { const res = promise.map(assert.ifError, token) cancel({}) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1141,7 +1141,7 @@ describe('future', () => { const res = promise.ap(fulfill(1), token) cancel({}) resolve(fulfill(assert.ifError)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1150,7 +1150,7 @@ describe('future', () => { const res = promise.ap(fulfill(1), token) cancel({}) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1161,7 +1161,7 @@ describe('future', () => { const res = promise.chain(assert.ifError, token) cancel({}) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1170,7 +1170,7 @@ describe('future', () => { const res = promise.chain(assert.ifError, token) cancel({}) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -1183,7 +1183,7 @@ describe('future', () => { const res = promise.then(assert.ifError, null, token) resolve(fulfill(1)) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1192,7 +1192,7 @@ describe('future', () => { const res = promise.then(assert.ifError, null, token) resolve(silenced(reject(1))) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1203,7 +1203,7 @@ describe('future', () => { const res = promise.catch(assert.ifError, token) resolve(fulfill(1)) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1212,7 +1212,7 @@ describe('future', () => { const res = promise.catch(assert.ifError, token) resolve(silenced(reject(1))) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1223,7 +1223,7 @@ describe('future', () => { const res = promise.map(assert.ifError, token) resolve(fulfill(1)) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1232,7 +1232,7 @@ describe('future', () => { const res = promise.map(assert.ifError, token) resolve(silenced(reject(1))) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1243,7 +1243,7 @@ describe('future', () => { const res = promise.ap(fulfill(1), token) resolve(fulfill(assert.ifError)) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1252,7 +1252,7 @@ describe('future', () => { const res = promise.ap(fulfill(1), token) resolve(silenced(reject(1))) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1263,7 +1263,7 @@ describe('future', () => { const res = promise.chain(assert.ifError, token) resolve(fulfill(1)) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1272,7 +1272,7 @@ describe('future', () => { const res = promise.chain(assert.ifError, token) resolve(silenced(reject(1))) cancel({}) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -1285,7 +1285,7 @@ describe('future', () => { promise.then(() => cancel({})) const res = promise.then(assert.ifError, null, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1294,7 +1294,7 @@ describe('future', () => { promise.catch(() => cancel({})) const res = promise.then(assert.ifError, null, token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1305,7 +1305,7 @@ describe('future', () => { promise.then(() => cancel({})) const res = promise.catch(assert.ifError, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1314,7 +1314,7 @@ describe('future', () => { promise.catch(() => cancel({})) const res = promise.catch(assert.ifError, token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1325,7 +1325,7 @@ describe('future', () => { promise.then(() => cancel({})) const res = promise.map(assert.ifError, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1334,7 +1334,7 @@ describe('future', () => { promise.catch(() => cancel({})) const res = promise.map(assert.ifError, token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1345,7 +1345,7 @@ describe('future', () => { promise.then(() => cancel({})) const res = promise.ap(fulfill(1), token) resolve(fulfill(assert.ifError)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1354,7 +1354,7 @@ describe('future', () => { promise.catch(() => cancel({})) const res = promise.ap(fulfill(1), token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1365,7 +1365,7 @@ describe('future', () => { promise.then(() => cancel({})) const res = promise.chain(assert.ifError, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for reject', () => { @@ -1374,7 +1374,7 @@ describe('future', () => { promise.catch(() => cancel({})) const res = promise.chain(assert.ifError, token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -1390,7 +1390,7 @@ describe('future', () => { throw new Error() }, null, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation for fulfill', () => { @@ -1398,7 +1398,7 @@ describe('future', () => { const { resolve, promise } = future() const res = promise.then(() => cancel({}), null, token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1408,7 +1408,7 @@ describe('future', () => { const { resolve, promise } = future() const res = promise.catch(() => cancel({}), token) resolve(silenced(reject(1))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1418,7 +1418,7 @@ describe('future', () => { const { resolve, promise } = future() const res = promise.map(() => cancel({}), token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1428,7 +1428,7 @@ describe('future', () => { const { resolve, promise } = future() const res = promise.ap(fulfill(1), token) resolve(fulfill(() => cancel({}))) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) @@ -1438,7 +1438,7 @@ describe('future', () => { const { resolve, promise } = future() const res = promise.chain(() => cancel({}), token) resolve(fulfill(1)) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) }) }) @@ -1625,7 +1625,7 @@ describe('then with nested callbacks', () => { const p = delay(1, {}) const res = p.then(() => delay(1), undefined, token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation when the outer promise is cancelled for same inner token', () => { @@ -1633,7 +1633,7 @@ describe('then with nested callbacks', () => { const p = delay(1, {}) const res = p.then(() => delay(1, null, token), undefined, token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation when the outer promise is cancelled for different inner token', () => { @@ -1641,7 +1641,7 @@ describe('then with nested callbacks', () => { const p = delay(1, {}) const res = p.then(() => delay(1, null, CancelToken.empty()), undefined, token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like fulfillment for no inner token', () => { @@ -1717,7 +1717,7 @@ describe('chain with nested callbacks', () => { const p = delay(1, {}) const res = p.chain(() => delay(1), token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation when the outer promise is cancelled for same inner token', () => { @@ -1725,7 +1725,7 @@ describe('chain with nested callbacks', () => { const p = delay(1, {}) const res = p.chain(() => delay(1, null, token), token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like cancellation when the outer promise is cancelled for different inner token', () => { @@ -1733,7 +1733,7 @@ describe('chain with nested callbacks', () => { const p = delay(1, {}) const res = p.chain(() => delay(1, null, CancelToken.empty()), token) p.then(cancel) - return assertSame(token.getRejected(), res) + return assertSame(token.getCancelled(), res) }) it('should behave like fulfillment for no inner token', () => { diff --git a/test/coroutine-test.js b/test/coroutine-test.js index b7ad769..94374bd 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { coroutine, fulfill, reject, delay, isRejected, CancelToken } from '../src/main' +import { coroutine, fulfill, reject, delay, isCancelled, CancelToken } from '../src/main' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -104,7 +104,7 @@ describe('coroutine', function () { const p = f(token) const d = delay(3, {}).then(() => { cancel() - assert(isRejected(p)) + assert(isCancelled(p)) assert(!executed, 'at yield') }) return delay(5, d).then(() => assert(executed, 'after yield in finally block')) @@ -121,7 +121,7 @@ describe('coroutine', function () { yield delay(1) try { cancel(expected) - rejected = isRejected(p) + rejected = isCancelled(p) yield executedT = true } finally { @@ -143,7 +143,7 @@ describe('coroutine', function () { const f = coroutine(function* () { coroutine.cancel = token }) - return assertSame(f(), token.getRejected()) + return assertSame(f(), token.getCancelled()) }) it('should not cancel when the last received token is not cancelled', () => { @@ -173,7 +173,7 @@ describe('coroutine', function () { const p = f(token) return delay(15).then(() => { cancel({}) - assert(isRejected(p)) + assert(isCancelled(p)) assert.strictEqual(counter, 0) }) }) @@ -216,7 +216,7 @@ describe('coroutine', function () { yield cancel() } finally { try { - assert(isRejected(p)) + assert(isCancelled(p)) assert.throws(() => coroutine.cancel, SyntaxError) assert.throws(() => { coroutine.cancel = null }, SyntaxError) } catch (e) { @@ -235,7 +235,7 @@ describe('coroutine', function () { yield delay(1) cancel() try { - assert(isRejected(p)) + assert(isCancelled(p)) assert.throws(() => { coroutine.cancel = null }, ReferenceError) assert.throws(() => { coroutine.cancel = CancelToken.empty() }, ReferenceError) } catch (e) { diff --git a/test/delay-test.js b/test/delay-test.js index 3e646ab..2772536 100644 --- a/test/delay-test.js +++ b/test/delay-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { delay, never, reject, fulfill, isRejected, isNever, isPending, CancelToken } from '../src/main' +import { delay, never, reject, fulfill, isCancelled, isNever, isPending, CancelToken } from '../src/main' import { Future, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -68,30 +68,30 @@ describe('delay', function () { const {token, cancel} = CancelToken.source() cancel({}) const p = delay(10, fulfill(1), token) - assert.strictEqual(token.getRejected(), p) + assert.strictEqual(token.getCancelled(), p) }) it('should return cancellation with cancelled token for reject', () => { const {token, cancel} = CancelToken.source() cancel({}) const p = delay(10, reject(1), token) - assert.strictEqual(token.getRejected(), p) + assert.strictEqual(token.getCancelled(), p) }) it('should behave like cancellation when cancelled for never', () => { const {token, cancel} = CancelToken.source() const p = delay(10, never(), token) cancel({}) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) it('should behave like cancellation when cancelled', () => { const {token, cancel} = CancelToken.source() const p = delay(10, fulfill(1), token) cancel({}) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) it('should behave like cancellation when cancelled during delay', () => { @@ -99,8 +99,8 @@ describe('delay', function () { const p = delay(10, fulfill(1), token) return delay(5).then(() => { cancel({}) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) }) @@ -109,8 +109,8 @@ describe('delay', function () { const p = delay(5, delay(10), token) return delay(5).then(() => { cancel({}) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) }) }) diff --git a/test/fulfill-test.js b/test/fulfill-test.js index aad8154..3d34565 100644 --- a/test/fulfill-test.js +++ b/test/fulfill-test.js @@ -40,7 +40,7 @@ describe('fulfill', () => { const p = fulfill(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.then(assert.ifError, assert.ifError, token)) + return assertSame(token.getCancelled(), p.then(assert.ifError, assert.ifError, token)) }) it('catch should be identity', () => { @@ -57,28 +57,28 @@ describe('fulfill', () => { const p = fulfill(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.catch(assert.ifError, token)) + return assertSame(token.getCancelled(), p.catch(assert.ifError, token)) }) it('map with cancelled token should behave like cancellation', () => { const p = fulfill(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.map(assert.ifError, token)) + return assertSame(token.getCancelled(), p.map(assert.ifError, token)) }) it('ap with cancelled token should behave like cancellation', () => { const p = fulfill(assert.ifError) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.ap(fulfill(true), token)) + return assertSame(token.getCancelled(), p.ap(fulfill(true), token)) }) it('chain with cancelled token should behave like cancellation', () => { const p = fulfill(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.chain(assert.ifError, token)) + return assertSame(token.getCancelled(), p.chain(assert.ifError, token)) }) it('trifurcate should be identity without f callback', () => { diff --git a/test/future-test.js b/test/future-test.js index d05ef8b..3b272b5 100644 --- a/test/future-test.js +++ b/test/future-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' import { future, reject, fulfill, isSettled, isPending, never, CancelToken } from '../src/main' -import { Future, silenceError } from '../src/Promise' +import { Future, cancel, silenceError } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -104,6 +104,13 @@ describe('future', () => { resolve(reject(expected)) return promise.then(assert.ifError, x => assert.strictEqual(expected, x)) }) + + it('should reject for cancelled promise', () => { + const { resolve, promise } = future() + const expected = {} + resolve(cancel(expected)) + return promise.trifurcate(assert.ifError, e => assert.strictEqual(e, expected), assert.ifError) + }) }) describe('when resolved to another promise', () => { diff --git a/test/inspect-test.js b/test/inspect-test.js index 4a084dd..01d7343 100644 --- a/test/inspect-test.js +++ b/test/inspect-test.js @@ -1,7 +1,7 @@ import { describe, it } from 'mocha' 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 { isFulfilled, isRejected, isCancelled, isSettled, isPending, isNever, getValue, getReason, isHandled } from '../src/inspect' +import { Future, cancel, silenceError } from '../src/Promise' import assert from 'assert' describe('inspect', () => { @@ -14,6 +14,10 @@ describe('inspect', () => { assert(!isFulfilled(reject())) }) + it('should be false for cancelled promise', () => { + assert(!isFulfilled(cancel())) + }) + it('should be false for pending promise', () => { assert(!isFulfilled(new Future())) }) @@ -28,6 +32,10 @@ describe('inspect', () => { assert(isRejected(reject())) }) + it('should be true for cancelled promise', () => { + assert(isRejected(cancel())) + }) + it('should be false for fulfilled promise', () => { assert(!isRejected(resolve())) }) @@ -41,6 +49,28 @@ describe('inspect', () => { }) }) + describe('isCancelled', () => { + it('should be true for cancelled promise', () => { + assert(isCancelled(cancel())) + }) + + it('should be false for fulfilled promise', () => { + assert(!isCancelled(resolve())) + }) + + it('should be false for rejected promise', () => { + assert(!isCancelled(reject())) + }) + + it('should be false for pending promise', () => { + assert(!isCancelled(new Future())) + }) + + it('should be false for never', () => { + assert(!isCancelled(never())) + }) + }) + describe('isSettled', () => { it('should be true for fulfilled promise', () => { assert(isSettled(resolve())) @@ -50,6 +80,10 @@ describe('inspect', () => { assert(isSettled(reject())) }) + it('should be true for cancelled promise', () => { + assert(isSettled(cancel())) + }) + it('should be false for pending promise', () => { assert(!isSettled(new Future())) }) @@ -68,6 +102,10 @@ describe('inspect', () => { assert(!isPending(reject())) }) + it('should be false for cancelled promise', () => { + assert(!isPending(cancel())) + }) + it('should be true for pending promise', () => { assert(isPending(new Future())) }) @@ -86,6 +124,10 @@ describe('inspect', () => { assert(!isNever(reject())) }) + it('should be false for cancelled promise', () => { + assert(!isNever(cancel())) + }) + it('should be false for pending promise', () => { assert(!isNever(new Future())) }) @@ -119,6 +161,10 @@ describe('inspect', () => { assert(isHandled(p)) }) + it('should be true for cancelled promise', () => { + assert(isHandled(cancel())) + }) + it('should be false for pending promise', () => { assert(!isHandled(new Future())) }) @@ -138,6 +184,10 @@ describe('inspect', () => { assert.throws(() => getValue(reject())) }) + it('should throw for cancelled promise', () => { + assert.throws(() => getValue(cancel())) + }) + it('should throw for pending promise', () => { assert.throws(() => getValue(new Future())) }) @@ -161,6 +211,11 @@ describe('inspect', () => { assert.strictEqual(x, getReason(reject(x))) }) + it('should get reason from cancelled promise', () => { + let x = {} + assert.strictEqual(x, getReason(cancel(x))) + }) + it('should throw for fulfilled promise', () => { assert.throws(() => getReason(fulfill())) }) diff --git a/test/never-test.js b/test/never-test.js index 5cb8cfb..82ef61d 100644 --- a/test/never-test.js +++ b/test/never-test.js @@ -11,7 +11,7 @@ describe('never', () => { it('then with token should return the cancellation', () => { const p = never() const token = CancelToken.empty() - assert.strictEqual(token.getRejected(), p.then(assert.ifError, assert.ifError, token)) + assert.strictEqual(token.getCancelled(), p.then(assert.ifError, assert.ifError, token)) }) it('catch should be identity', () => { @@ -22,7 +22,7 @@ describe('never', () => { it('catch with token should return the cancellation', () => { const p = never() const token = CancelToken.empty() - assert.strictEqual(token.getRejected(), p.catch(assert.ifError, token)) + assert.strictEqual(token.getCancelled(), p.catch(assert.ifError, token)) }) it('map should be identity', () => { @@ -33,7 +33,7 @@ describe('never', () => { it('map with token should return the cancellation', () => { const p = never() const token = CancelToken.empty() - assert.strictEqual(token.getRejected(), p.map(assert.ifError, token)) + assert.strictEqual(token.getCancelled(), p.map(assert.ifError, token)) }) it('ap should be identity', () => { @@ -44,7 +44,7 @@ describe('never', () => { it('ap with token should return the cancellation', () => { const p = never() const token = CancelToken.empty() - assert.strictEqual(token.getRejected(), p.ap(fulfill(true), token)) + assert.strictEqual(token.getCancelled(), p.ap(fulfill(true), token)) }) it('chain should be identity', () => { @@ -55,7 +55,7 @@ describe('never', () => { it('chain with token should return the cancellation', () => { const p = never() const token = CancelToken.empty() - assert.strictEqual(token.getRejected(), p.chain(assert.ifError, token)) + assert.strictEqual(token.getCancelled(), p.chain(assert.ifError, token)) }) it('finally should be identity', () => { diff --git a/test/reject-test.js b/test/reject-test.js index 8807509..46d410d 100644 --- a/test/reject-test.js +++ b/test/reject-test.js @@ -21,14 +21,14 @@ describe('reject', () => { const p = reject(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.then(assert.ifError, null, token)) + return assertSame(token.getCancelled(), p.then(assert.ifError, null, token)) }) it('catch with cancelled token should behave like cancellation', () => { const p = reject(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.catch(assert.ifError, token)) + return assertSame(token.getCancelled(), p.catch(assert.ifError, token)) }) it('map should be identity', () => { @@ -47,7 +47,7 @@ describe('reject', () => { const p = reject(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.map(assert.ifError, token)) + return assertSame(token.getCancelled(), p.map(assert.ifError, token)) }) it('ap should be identity', () => { @@ -66,7 +66,7 @@ describe('reject', () => { const p = reject(assert.ifError) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.ap(fulfill(true), token)) + return assertSame(token.getCancelled(), p.ap(fulfill(true), token)) }) it('chain should be identity', () => { @@ -85,7 +85,7 @@ describe('reject', () => { const p = reject(true) const {token, cancel} = CancelToken.source() cancel({}) - return assertSame(token.getRejected(), p.chain(assert.ifError, token)) + return assertSame(token.getCancelled(), p.chain(assert.ifError, token)) }) it('trifurcate should be identity without r callback', () => { diff --git a/test/resolve-test.js b/test/resolve-test.js index abef094..0710e91 100644 --- a/test/resolve-test.js +++ b/test/resolve-test.js @@ -1,6 +1,6 @@ import { describe, it } from 'mocha' -import { resolve, fulfill, reject, future, isRejected, CancelToken } from '../src/main' -import { Future } from '../src/Promise' +import { resolve, fulfill, reject, future, isCancelled, CancelToken } from '../src/main' +import { Future, cancel } from '../src/Promise' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -75,25 +75,25 @@ describe('resolve', () => { it('should return cancellation with cancelled token for true', () => { const {token, cancel} = CancelToken.source() cancel({}) - assert.strictEqual(token.getRejected(), resolve(true, token)) + assert.strictEqual(token.getCancelled(), resolve(true, token)) }) it('should return cancellation with cancelled token for future', () => { const {token, cancel} = CancelToken.source() cancel({}) - assert.strictEqual(token.getRejected(), resolve(new Future(), token)) + assert.strictEqual(token.getCancelled(), resolve(new Future(), token)) }) it('should return cancellation with cancelled token for fulfill', () => { const {token, cancel} = CancelToken.source() cancel({}) - assert.strictEqual(token.getRejected(), resolve(fulfill(), token)) + assert.strictEqual(token.getCancelled(), resolve(fulfill(), token)) }) it('should return cancellation with cancelled token for reject', () => { const {token, cancel} = CancelToken.source() cancel({}) - assert.strictEqual(token.getRejected(), resolve(reject(), token)) + assert.strictEqual(token.getCancelled(), resolve(reject(), token)) }) it('should be identity for future with same token', () => { @@ -107,9 +107,9 @@ describe('resolve', () => { const {promise} = future() const p = resolve(promise, token) cancel({}) - assert(!isRejected(promise)) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(!isCancelled(promise)) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) it('should cancel result for unresolved promise with different token', () => { @@ -117,9 +117,9 @@ describe('resolve', () => { const {promise} = future(CancelToken.empty()) const p = resolve(promise, token) cancel({}) - assert(!isRejected(promise)) - assert(isRejected(p)) - return assertSame(token.getRejected(), p) + assert(!isCancelled(promise)) + assert(isCancelled(p)) + return assertSame(token.getCancelled(), p) }) }) @@ -137,4 +137,11 @@ describe('resolve', () => { const p = future().promise assert.strictEqual(resolve(p), p) }) + + it('should reject for cancelled promise', () => { + const expected = {} + return resolve(cancel(expected)).trifurcate(assert.ifError, e => { + assert.strictEqual(e, expected) + }, assert.ifError) + }) }) diff --git a/test/trifurcate-test.js b/test/trifurcate-test.js index e0204e2..8bf221f 100644 --- a/test/trifurcate-test.js +++ b/test/trifurcate-test.js @@ -1,32 +1,51 @@ import { describe, it } from 'mocha' import { future, fulfill, reject, never, CancelToken, all } from '../src/main' -import { silentReject } from '../src/Promise' import { assertSame, raceCallbacks } from './lib/test-util' import assert from 'assert' describe('untilCancel', () => { - it('should always return a promise with the token on its .token property', () => { + const testResult = t => f => () => { + const token = t() + const res = f(token).untilCancel(token) + try { + assert.strictEqual(res.token, token) + } catch (e) { + assert.strictEqual(res, token.getCancelled()) + } + } + const testUncancelled = testResult(() => CancelToken.empty()) + const testCancelled = testResult(() => { const {token, cancel} = CancelToken.source() - - assert.strictEqual(never().untilCancel(token).token, token, 'never') - assert.strictEqual(future().promise.untilCancel(token).token, token, 'unresolved future') - assert.strictEqual(future(token).promise.untilCancel(token).token, token, 'unresolved future with same token') - assert.strictEqual(future(CancelToken.empty()).promise.untilCancel(token).token, token, 'unresolved future with other token') - cancel({}) + return token + }) - assert.strictEqual(fulfill().untilCancel(token).token, token, 'fulfill') - assert.strictEqual(reject().untilCancel(token).token, token, 'reject') - assert.strictEqual(never().untilCancel(token).token, token, 'never') - assert.strictEqual(future().promise.untilCancel(token).token, token, 'unresolved future') - const a = future() - a.resolve(fulfill()) - assert.strictEqual(a.promise.untilCancel(token).token, token, 'fulfilled future') - const b = future() - b.resolve(reject()) - assert.strictEqual(b.promise.untilCancel(token).token, token, 'rejected future') - const c = future(token) - assert.strictEqual(c.promise.untilCancel(token).token, token, 'future with token') + describe('returns cancellation or promise with token for uncancelled token on', () => { + it('never', testUncancelled(() => never())) + it('unresolved future', testUncancelled(() => future().promise)) + it('unresolved future with same token', testUncancelled(token => future(token).promise)) + it('unresolved future with other token', testUncancelled(() => future(CancelToken.empty()).promise)) + }) + + describe('returns cancellation or promise with token for cancelled token on', () => { + it('fulfill', testCancelled(fulfill)) + it('reject', testCancelled(reject)) + it('never', testCancelled(never)) + it('unresolved future', testCancelled(() => future().promise)) + it('fulfilled future', testCancelled(() => { + const f = future() + f.resolve(fulfill()) + return f.promise + })) + it('rejected future', testCancelled(() => { + const f = future() + f.resolve(reject()) + return f.promise + })) + it('unresolved future with token', testCancelled(token => { + const f = future(token) + return f.promise + })) }) }) @@ -56,7 +75,7 @@ describe('trifurcate', () => { const f = x => x + 1 const fp = x => fulfill(x + 1) - const rp = x => silentReject(x + 1) + const rp = x => reject(x + 1) const tr = x => { throw x + 1 } describe('on fulfilled future', () => { From b1ad38434c45144fa42581919f067d1ada4c383b Mon Sep 17 00:00:00 2001 From: Bergi Date: Wed, 13 Jul 2016 00:30:37 +0200 Subject: [PATCH 26/28] Docs, docs, docs! --- README.md | 195 ++++++++++++++++++++------ cancellation.md | 353 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+), 45 deletions(-) create mode 100644 cancellation.md diff --git a/README.md b/README.md index 1b45d98..534fcc9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# creed :: async +# creed :: async [![Join the chat at https://gitter.im/briancavalier/creed](https://badges.gitter.im/briancavalier/creed.svg)](https://gitter.im/briancavalier/creed?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Sophisticated and functionally-minded async with advanced features: coroutines, promises, ES2015 iterables, [fantasy-land](https://github.com/fantasyland/fantasy-land). +Sophisticated and functionally-minded async with advanced features: promises, [cancellation](cancellation.md), coroutines, ES2015 iterables, [fantasy-land](https://github.com/fantasyland/fantasy-land). -Creed simplifies async by letting you write coroutines using ES2015 generators and promises, and encourages functional programming via fantasy-land. It also makes uncaught errors obvious by default, and supports other ES2015 features such as iterables. +Creed simplifies async by letting you write coroutines using ES2015 generators and promises, and encourages functional programming via fantasy-land. It also makes uncaught errors obvious by default, and supports other ES2015 features such as iterables. It empowers you to direct the execution flow in detail through cancellation of callbacks. You can also use [babel](https://babeljs.io) and the [babel-creed-async](https://github.com/briancavalier/babel-creed-async) plugin to write ES7 `async` functions backed by creed coroutines. @@ -18,8 +18,6 @@ You can also use [babel](https://babeljs.io) and the [babel-creed-async](https:/ Using creed coroutines, ES2015, and FP to solve the [async-problem](https://github.com/plaid/async-problem): ```javascript -'use strict'; - import { runNode, all, coroutine } from 'creed'; import { readFile } from 'fs'; import { join } from 'path'; @@ -103,7 +101,7 @@ Promise { fulfilled: winner } # Errors & debugging -By design, uncaught creed promise errors are fatal. They will crash your program, forcing you to fix or [`.catch`](#catch--promise-e-a--e--bpromise-e-b--promise-e-b) them. You can override this behavior by [registering your own error event listener](#debug-events). +By design, uncaught creed promise errors are fatal. They will crash your program, forcing you to fix or [`.catch`](#catch--promise-e-a--e--bthenable-e-b--canceltoken-e--promise-e-b) them. You can override this behaviour by [registering your own error event listener](#debug-events). Consider this small program, which contains a `ReferenceError`. @@ -214,9 +212,9 @@ function reportHandled(promise) { ## Run async tasks -### coroutine :: Generator a → (...* → Promise e a) +### coroutine :: GeneratorFunction a → (...* → Promise e a) -Create an async coroutine from a promise-yielding generator. +Create an async coroutine from a promise-yielding generator function. ```js import { coroutine } from 'creed'; @@ -226,7 +224,7 @@ function fetchTextFromUrl(url) { return promise; } -// Make an async coroutine from a generator +// Make an async coroutine from a generator function let getUserProfile = coroutine(function* (userId) { try { let profileUrl = yield getUserProfileUrlFromDB(userId); @@ -242,6 +240,8 @@ getUserProfile(123) .then(profile => console.log(profile)); ``` +For cancellation of coroutines see the [cancellation docs](cancellation.md#coroutines). + ### fromNode :: NodeApi e a → (...* → Promise e a) type NodeApi e a = ...* → Nodeback e a → ()
type Nodeback e a = e → a → () @@ -283,7 +283,7 @@ Run a function to produce a promised result. ```js import { runPromise } from 'creed'; -/* Run a function, threading in a url parameter */ +/* Run a function, passing in a url parameter */ let p = runPromise((url, resolve, reject) => { var xhr = new XMLHttpRequest; xhr.addEventListener("error", reject); @@ -295,7 +295,7 @@ let p = runPromise((url, resolve, reject) => { p.then(result => console.log(result)); ``` -Parameter threading also makes it easy to create reusable tasks +Parameter passing also makes it easy to create reusable tasks that don't rely on closures and scope chain capturing. ```js @@ -313,28 +313,21 @@ runPromise(xhrGet, 'http://...') .then(result => console.log(result)); ``` -### merge :: (...* → b) → ...Promise e a → Promise e b - -Merge promises by passing their fulfillment values to a merge -function. Returns a promise for the result of the merge function. -Effectively liftN for promises. - -```js -import { merge, resolve } from 'creed'; +### new Promise :: Producer e a [→ CancelToken e] → Promise e a -merge((x, y) => x + y, resolve(123), resolve(1)) - .then(z => console.log(z)); //=> 124 -``` +ES6-compliant promise constructor. Run an executor function to produce a promised result. +If the optional cancellation token is passed, it will be associated to the promise. ## Make promises -### future :: () → { resolve: Resolve e a, promise: Promise e a } +### future :: [CancelToken e] → { resolve :: Resolve e a, promise :: Promise e a } type Resolve e a = a|Thenable e a → ()
Create a `{ resolve, promise }` pair, where `resolve` is a function that seals the fate of `promise`. +If the optional cancellation token is passed, it will be associated to the `promise`. ```js -import { future, reject } from 'creed'; +import { future, reject, CancelToken } from 'creed'; // Fulfill let { resolve, promise } = future(); @@ -350,11 +343,18 @@ resolve(anotherPromise); //=> make promise's fate the same as anotherPromise's let { resolve, promise } = future(); resolve(reject(new Error('oops'))); promise.catch(e => console.log(e)); //=> [Error: oops] + +// Cancel +let { cancel, token } = CancelToken.source(); +let { resolve, promise } = future(token); +cancel(new Error('already done')); +promise.trifurcate(null, null, e => console.log(e)); //=> [Error: already done] ``` -### resolve :: a|Thenable e a → Promise e a +### resolve :: a|Thenable e a [→ CancelToken e] → Promise e a -Coerce a value or Thenable to a promise. +Coerce a value or thenable to a promise. +If the optional cancellation token is passed, it will be associated to the promise. ```js import { resolve } from 'creed'; @@ -387,7 +387,11 @@ resolve(fulfill(123)) .then(x => console.log(x)); //=> 123 ``` -### reject :: Error e => e → Promise e a +### Promise.of :: a → Promise e a + +Alias for `fulfill`, completing the [Fantasy-land Applicative](//github.com/fantasyland/fantasy-land#applicative). + +### reject :: Error e ⇒ e → Promise e a Make a rejected promise for an error. @@ -398,7 +402,7 @@ reject(new TypeError('oops!')) .catch(e => console.log(e.message)); //=> oops! ``` -### never :: Promise e a +### never :: () → Promise e a Make a promise that remains pending forever. @@ -410,16 +414,23 @@ never() ``` Note: `never` consumes virtually no resources. It does not hold references -to any functions passed to `then`, `map`, `chain`, etc. +to any functions passed to `then`, `map`, `chain`, etc. + +### Promise.empty :: () → Promise e a + +Alias for `never`, completing the [Fantasy-land Monoid](//github.com/fantasyland/fantasy-land#monoid). ## Transform promises -### .then :: Promise e a → (a → b|Promise e b) → Promise e b +### .then :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) [→ CancelToken e] → Promise e b [Promises/A+ then](http://promisesaplus.com/). -Transform a promise's value by applying a function to the -promise's fulfillment value. Returns a new promise for the -transformed result. +Transform a promise's value by applying the first function to the +promise's fulfillment value or the second function to the rejection reason. +Returns a new promise for the transformed result. +If the respective argument is no function, the resolution is passed through. +If the optional cancellation token is passed, it will be associated to the result promise. +The callbacks will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -433,9 +444,11 @@ resolve(1) .then(y => console.log(y)); //=> 2 ``` -### .catch :: Promise e a → (e → b|Promise e b) → Promise e b +### .catch :: Promise e a → (e → b|Thenable e b) [→ CancelToken e] → Promise e b -Catch and handle a promise error. +Catch and handle a promise error. Equivalent to `.then(undefined, onRejected)`. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { reject, resolve } from 'creed'; @@ -449,12 +462,14 @@ reject(new Error('oops!')) .then(x => console.log(x)); //=> 123 ``` -### .map :: Promise e a → (a → b) → Promise e b +### .map :: Promise e a → (a → b) [→ CancelToken e] → Promise e b [Fantasy-land Functor](https://github.com/fantasyland/fantasy-land#functor). Transform a promise's value by applying a function. The return value of the function will be used verbatim, even if it is a promise. Returns a new promise for the transformed value. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -464,11 +479,13 @@ resolve(1) .then(y => console.log(y)); //=> 2 ``` -### .ap :: Promise e (a → b) → Promise e a → Promise e b +### .ap :: Promise e (a → b) → Promise e a [→ CancelToken e] → Promise e b [Fantasy-land Apply](https://github.com/fantasyland/fantasy-land#apply). Apply a promised function to a promised value. Returns a new promise for the result. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js import { resolve } from 'creed'; @@ -483,11 +500,13 @@ resolve(x => y => x+y) .then(y => console.log(y)); //=> 124 ``` -### .chain :: Promise e a → (a → Promise e b) → Promise e b +### .chain :: Promise e a → (a → Promise e b) [→ CancelToken e] → Promise e b [Fantasy-land Chain](https://github.com/fantasyland/fantasy-land#chain). Sequence async actions. When a promise fulfills, run another async action and return a promise for its result. +If the optional cancellation token is passed, it will be associated to the result promise. +The callback will never run after cancellation has been requested. ```js let profileText = getUserProfileUrlFromDB(userId) @@ -511,16 +530,72 @@ fulfill(123).concat(fulfill(456)) .then(x => console.log(x)); //=> 123 ``` +### .untilCancel :: Promise e a → CancelToken e → Promise e a + +Returns a promise equivalent to the receiver, but with the token associated to it. Equivalent to `.then(null, null, token)`. +Essentially the cancellation is raced against the resolution. +Preference is given to the former, it always returns a cancelled promise if the token is already revoked. + +### .trifurcate :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) → (e → b|Thenable e b) → Promise e b + +Transform a promise's value by applying the first function to the +promise's fulfillment value, the second function to the rejection reason +or the third function to the cancellation reason if the promise was rejected through its associated token. +Returns a new promise for the transformed result, with no cancellation token associated. +If the respective argument is no function, the resolution is passed through. + +It is guaranteed that at most one of the callbacks is called. +It can happen that the `onFulfilled` or `onRejected` callbacks run despite the cancellation having been requested. + +``` +import { delay, CancelToken } from 'creed'; + +const { cancel, token } = CancelToken.source(); +setTimeout(() => { + cancel(new Error('timeout')); +}, 2000); + +fetch(…).untilCancel(token) // better: fetch(…, token) + .trifurcate(x => console.log('result', x), e => console.error(e), e => console.log('cancel', e)); +``` + +### .finally :: Promise e a → (Promise e a → b|Thenable e b) → Promise e a + +Runs the function when the promise settles or its associated token is revoked. +The resolution is not transformed, the callback result is awaited but ignored, unless it rejects. +The returned promise has no cancellation token associated to it. + +In case of cancellation, the callback is executed synchronously like a token subscription, its return value is yielded to the `cancel()` caller. + +### merge :: (...* → b) → ...Promise e a → Promise e b + +Merge promises by passing their fulfillment values to a merge +function. Returns a promise for the result of the merge function. +Effectively liftN for promises. + +```js +import { merge, resolve } from 'creed'; + +merge((x, y) => x + y, resolve(123), resolve(1)) + .then(z => console.log(z)); //=> 124 +``` + +## Cancellation + +For the `CancelToken` documentation see the separate [cancellation API description](cancellation.md#api). + ## Control time -### delay :: Int → a|Promise e a → Promise e a +### delay :: Int → a|Promise e a [→ CancelToken e] → Promise e a Create a delayed promise for a value, or further delay the fulfillment of an existing promise. Delay only delays fulfillment: it has no effect on rejected promises. +If the optional cancellation token is passed, it will be associated to the result promise. +When the cancellation is requested, the timeout is cleared. ```js -import { delay, reject } from 'creed'; +import { delay, reject, CancelToken } from 'creed'; delay(5000, 'hi') .then(x => console.log(x)); //=> 'hi' after 5 seconds @@ -530,6 +605,11 @@ delay(5000, delay(1000, 'hi')) delay(5000, reject(new Error('oops'))) .catch(e => console.log(e.message)); //=> 'oops' immediately + +const { cancel, token } = CancelToken.source(); +delay(2000, 'over').then(cancel); +delay(5000, 'result', token) + .catch(e => console.log(e)); //=> 'over' after 2 seconds ``` ### timeout :: Int → Promise e a → Promise e a @@ -552,7 +632,7 @@ Creed's iterable functions accept any ES2015 Iterable. Most of the examples in this section show Arrays, but Sets, generators, etc. will work as well. -### all :: Iterable (Promise e a) → Promise e [a] +### all :: Iterable (Promise e a) → Promise e (Array a) Await all promises from an Iterable. Returns a promise that fulfills with an array containing all input promise fulfillment values, @@ -627,7 +707,7 @@ any([]) .catch(e => console.log(e)); //=> [RangeError: No fulfilled promises in input] ``` -### settle :: Iterable (Promise e a) → Promise e [Promise e a] +### settle :: Iterable (Promise e a) → Promise e (Array (Promise e a)) Returns a promise that fulfills with an array of settled promises. @@ -647,12 +727,14 @@ settle([resolve(123), reject(new Error('oops')), resolve(456)]) Returns true if the promise is fulfilled. ```js -import { isFulfilled, resolve, reject, delay, never } from 'creed'; +import { isFulfilled, resolve, reject, delay, never, CancelToken } from 'creed'; +const token = new CancelToken(cancel => cancel()); isFulfilled(resolve(123)); //=> true isFulfilled(reject(new Error())); //=> false isFulfilled(delay(0, 123)); //=> true isFulfilled(delay(1, 123)); //=> false +isFulfilled(token.getCancelled());//=> false isFulfilled(never()); //=> false ``` @@ -667,6 +749,7 @@ isRejected(resolve(123)); //=> false isRejected(reject(new Error())); //=> true isRejected(delay(0, 123)); //=> false isRejected(delay(1, 123)); //=> false +isRejected(token.getCancelled());//=> true isRejected(never()); //=> false ``` @@ -681,6 +764,7 @@ isSettled(resolve(123)); //=> true isSettled(reject(new Error())); //=> true isSettled(delay(0, 123)); //=> true isSettled(delay(1, 123)); //=> false +isSettled(token.getCancelled());//=> true isSettled(never()); //=> false ``` @@ -698,6 +782,26 @@ isPending(delay(1, 123)); //=> true isPending(never()); //=> true ``` +### isCancelled :: Promise e a → boolean + +Returns true if the promise is rejected because cancellation was requested through its associated token. + +``` +import { isFulfilled, resolve, reject, delay, never, CancelToken } from 'creed'; +const cancelledToken = new CancelToken(cancel => cancel()); +const { cancel, token } = CancelToken.source() +const p = future(token).promise + +isCancelled(resolve(123)); //=> false +isCancelled(reject(new Error())); //=> false +isCancelled(delay(1, 123)); //=> false +isCancelled(future(cancelledToken).promise); //=> true +isCancelled(delay(0, 123, cancelledToken)); //=> true +isCancelled(p); //=> false +cancel(); +isCancelled(p); //=> true +``` + ### isNever :: Promise e a → boolean Returns true if it is known that the promise will remain pending @@ -711,6 +815,7 @@ isNever(resolve(123)); //=> false isNever(reject(new Error())); //=> false isNever(delay(0, 123)); //=> false isNever(delay(1, 123)); //=> false +isNever(token.getCancelled()); //=> false isNever(never()); //=> true isNever(resolve(never())); //=> true isNever(delay(1000, never())); //=> true @@ -747,8 +852,8 @@ getReason(never()); //=> throws TypeError ### shim :: () → PromiseConstructor|undefined -Polyfill the global `Promise` constructor with an ES6-compliant -creed `Promise`. If there was a pre-existing global `Promise`, +Polyfill the global `Promise` constructor with the [creed `Promise` constructor](#new-promise--producer-e-a--canceltoken-e--promise-e-a). +If there was a pre-existing global `Promise`, it is returned. ```js diff --git a/cancellation.md b/cancellation.md new file mode 100644 index 0000000..35cf2f9 --- /dev/null +++ b/cancellation.md @@ -0,0 +1,353 @@ +Creed features cancellation with a cancellation token based approach. + +# Terminology + +1. A **cancellation token** (**`CancelToken`**) is an object with methods for determining whether and when an operation should be cancelled. +2. A **revoked token** is a `CancelToken` that is in the cancelled state, denoting that the result of an operation is no longer of interest. +3. The cancellation can be **requested** by the issuer of the `CancelToken`, thereby revoking it. +4. A **cancellation reason** is a value used to request a cancellation and reject the respective promises. +5. One `CancelToken` might be **associated** with a promise. +6. A **cancelled promise** is a promise that got rejected because its associated token has been revoked. +7. A **cancelled callback** is an `onFulfilled` or `onRejected` handler whose corresponding cancellation token has been revoked. It might be considered an **unregistered** or **ignored** callback. + +# Cancelling… + +Cancellation allows you to stop asynchronous operations built with promises. Use cases might both be in programmatical cancellation, where your program stops doing things after e.g. a timeout has expired or another operation has finished earlier, and in interactive cancellation, where a user triggers the stop through input methods. + +Operations that are supposed to be stoppable must support this explicitly. It is not desired that anyone who holds a promise can cancel the operation that computes the result, therefore the invoker of the operation has to pass in a cancellation token that only he can revoke to request the cancellation. +Passing around this capability explicitly can be a bit verbose at times, but everything else is done by Creed for you. + +## …Promises + +A promise can be cancelled through a cancellation token at any time before it is fulfilled or rejected. For this, the token is associated with the promise. The `new Promise` constructor, the `future` factory and the `resolve` function support this via an optional parameter: +```javascript +import { future, Promise, CancelToken } from 'creed'; + +const token = new CancelToken(…); +var cancellablePromise = new Promise(…, token); +var cancellableFuture = future(token); +var cancellableResolution = resolve(…, token); +``` +Many of the builtin methods also return promises that have a cancellation token associated with them. + +The token that is associated with a promise cannot be changed or removed afterwards (see [`CancelToken.reference`](#canceltokenreference--canceltoken-r--reference-r) for an alternative). +When the cancellation is requested, all promises that are associated to the token become immediately rejected unless they are already settled. +The rejection reason will be the one that is given as the reason to the cancellation request. +Notice that even promises that already have been resolved to another promise but are still not settled will be cancelled: +```javascript +import { delay, CancelToken } from 'creed'; + +const { token, cancel } = CancelToken.source(); +delay(3000, 'over').then(cancel); + +const { promise, resolve } = future(token); +resolve(delay(5000, 'result')); +promise.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds +``` + +If you want to associate a token to an already existing promise, you can use the `.untilCancel(token)` method, although this is rarely necessary. + +## …Callbacks + +The most important feature to avoid unnecessary work and to ignore the results of any promise is to prevent callbacks from running. +The main [transformation methods](README.md#transform-promises) (`then`, `catch`, `map`, `ap`, `chain`) have an optional token parameter for this in Creed. +The cancellation token is registered together with the callback that are to be executed when the promise fulfills or rejects. +As soon as the cancellation is requested, the respective callbacks are guaranteed not to be invoked any more (even when the promise is already fulfilled or rejected). The callbacks are "unregistered" or "cancelled" through this. + +The passed token is associated with the returned promise. +```javascript +import { delay, CancelToken } from 'creed'; + +const { token, cancel } = CancelToken.source(); +delay(3000, 'over').then(cancel); + +const p = delay(1000).chain(x => delay(4000, 'result'), token); +// the token is associated with p, so despite p being resolved with the delay we get +p.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds + +const q = delay(4000).chain(x => { + console.log('never executed'); + return delay(1000, 'result'); +}, token); +// the token being revoked prevents the inner delay from ever being created, and we get +q.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds +``` + +### Usage + +As a rule of thumb, take + +> You will normally want to pass the token +> +> * to every asynchronous function you call +> * to every transformation method you invoke +> +> or in short, to everything that returns a promise + +A typical function might therefore look like +```javascript +function load(url, token) { + return fetch(url, token) + .then(response => response.readText(token), token) + .map(JSON.parse, token) + .then(d => getDetails(d.result, token), token) + .catch(e => reject(new WrapError('fetching problem', e)), token); +} +``` +When the cancellation is requested, every promise in the chain (that is not already settled) will be rejected, +and at the same time none of the callbacks (that did not already run) will ever be executed. +If the caller of `load` does not intend to cancel it, he would just pass no `token` (or `undefined` or `null`) and the chain would not be cancellable. + +If you want a strand of actions to run without being cancelled after they have begun, just omit the `token` for them. +Beware of the usage of `.catch` without a token however, it would catch the cancellation reason then, so if you need to deal with exceptions in there better nest: +```javascript +function notCancellable(…) { + return …; // no token within here +} +function partiallyCancellable(…, token) { + return … // use token here + .chain(notCancellable, token) + …; // and there +} +``` + +If an API you are calling does not support cancellation, you of course don't have to pass it a token either. +Just `resolve` it to a Creed promise and attach your callbacks with a token, which means the operation will continue but be ignored when cancellation is requested. + +### finally + +The `finally` method is a helper for ensuring a callback always gets called. It does work a bit like +```javascript +Promise.prototype.finally = function(f) { + const g = () => resolve(f(this)).then(() => this) + return this.then(g, g) +}; +``` +but in contrast to a regular `onRejected` handler without a token it does get called synchronously from a cancellation request on the associated token of `this`, +yielding the result of the `f` call to the canceller so that he might handle possible exceptions which otherwise are usually ignored. + +You can use it for something like +```javascript +startSpinner(); +const token = new CancelToken(showStopbutton); +const p = load('http://…', token) +p.finally(() => { + stopSpinner(); + hideStopbutton(); +}).then(showResult, showErrormessage, token); +``` + +### trifurcate + +Sometimes you want to distinguish whether a promise was fulfilled, rejected, or cancelled through its associated token. +You could do it with synchronous inspection in a `finally` handler, but there is an easier way. +The `trifurcate` method is essentially equivalent to +```javascript +Promise.prototype.trifurcate = function(onFulfilled, onRejected, onCancelled) { + return this.then(onFulfilled, r => (isCancelled(this) ? onCancelled : onRejected)(r)); +}; +``` +You can use it for something like +```javascript +const token = new CancelToken(cancel => { + setTimeout(cancel, 3000) +}); +load('http://…', token).trifurcate(showResult, showErrormessage, showTimeoutmessage); +``` + +## …Coroutines + +Coroutines work with cancellation as well. They simplify dealing with cancellation tokens just like they avoid callbacks. +The above example would read +```javascript +const load = coroutine(function* (url, token) { + coroutince.cancel = token; + try { + const response = yield fetch(url, token); + const d = JSON.parse(yield response.readText(token)); + return yield getDetails(d.result, token); + } catch (e) { + throw new WrapError('fetching problem', e)); + } +}); +``` +You still would have to pass the token to all promise-returning asynchronous function calls, but there are no callbacks any more that you have to register the token with. +Instead, the magic `coroutine.cancel` setter allows you to choose the cancellation token that is used while waiting for each `yield`ed promise. +If the cancellation is requested during the time a promise is awaited, the coroutine will abort and immediately return a completion from the `yield` expression that does only trigger `finally` blocks in the generator function. The promise returned by the coroutine will be rejected like if the token was associated to it. + +This does allow for quite classical patterns: +```javascript +coroutine.cancel = token; +const conn = db.open(); +try { + … yield conn.query(…, token); + return … +} finally { + conn.close(); +} +``` +where the connection is always closed, even when the `token` is revoked during the query. + +It does also make it possible to react specifically to cancellation during a strand of execution in a coroutine: +```javascript +coroutine.cancel = token; +try { + … +} finally { + if (token.requested) { + … // cancelled during a yield in the try block + } +} +``` + +It is also possible to change the `coroutine.cancel` token during the execution of a coroutine: +```javascript +coroutine.cancel = token; +… // uses token here when yielding +coroutine.cancel = null; +… // not cancellable during this section +if (token.requested) …; // manually checking for cancellation +… +coroutine.cancel = token; +yield; // immediately abort if already cancelled +… // uses token here again +``` +The end of the uncancellable section can also be combined into a single `yield coroutine.cancel = token;` statement. + +On accessing, the magic `coroutine.cancel` getter returns the `CancelToken` that is associated with the promise returned by the coroutine. + +# API + +## Create tokens + +### new CancelToken :: ((r → ()) → ()) → CancelToken r + +Calls an executor callback with a function that allows to cancel the created `CancelToken`. + +### CancelToken.source :: () → { cancel :: r → (), token :: CancelToken r } + +Creates a `{ token, cancel }` pair where `token` is a new `CancelToken` and `cancel` is a function to request cancellation. + +### CancelToken.for :: Thenable _ r → CancelToken r + +Creates a cancellation token that is requested when the input promise fulfills. + +### CancelToken.empty :: () → CancelToken _ + +Creates a cancellation token that is never requested, completing the [Fantasy-land Monoid](//github.com/fantasyland/fantasy-land#monoid). + +## Subscribe + +### .requested :: CancelToken r → boolean + +Synchronously determines whether the token is revoked. +```javascript +const { token, cancel } = CancelToken.source(); +console.log(token.requested); //=> false +cancel(); +console.log(token.requested); //=> true +``` + +### .getCancelled :: CancelToken r → Promise r _ + +Returns a promise with this token associated, i.e. one that rejects when the cancellation is requested. Allows for asynchronous subscription: +```javascript +const { token, cancel } = CancelToken.source(); +token.getCancelled().then(null, e => console.log(e)); +token.getCancelled().catch(e => console.log(e)); +token.getCancelled().trifurcate(null, null, e => console.log(e)); +cancel('reason'); +//=> reason, reason, reason +``` + +### .subscribe :: CancelToken r → (r → a|Thenable e a) → Promise e a + +Transforms the token's cancellation reason by applying the function to it. +Returns a promise for the transformed result. +The callback is invoked synchronously from a cancellation request, returning the promise also to the canceller. +If the token is already revoked, the callback is invoked asynchronously. +```javascript +const { token, cancel } = CancelToken.source(); +const p = token.subscribe(r => r + ' accepted'); +const q = token.subscribe(r => { throw new Error(…); }); +console.log(cancel('reason')); //=> [ Fulfilled { value: "reason accepted" }, Rejected { value: Error {…} } ] +p.then(x => console.log(x)); //=> reason accepted +``` + +### .subscribeOrCall :: CancelToken r → (r → a|Thenable e a) [→ (...* → b)] → (...* → [b]) + +Subscribes the callback to be cancelled synchronously from a cancellation request or asynchronously when the token is already revoked. +Returns a function that unsubscribes the callback. + +Unless the callback has already been executed, if the optional second parameter is a function it will be invoked at most once with the unsubscription arguments. +```javascript +const { token, cancel } = CancelToken.source(); +const a = token.subscribeOrCall(r => r + ' accepted', () => console.log('never executed')); +const b = token.subscribeOrCall(r => console.log('never executed'), x => console.log('executed ' + x)); +const c = token.subscribeOrCall(r => { throw new Error(…); }); +b('once'); //=> executed once +b('twice'); // nothing happens +console.log(cancel('reason')); //=> [ Fulfilled { value: "reason accepted" }, Rejected { value: Error {…} } ] +a(); // nothing happens +b(); // still nothing +``` + +This is an especially helpful tool in the promisification of cancellable APIs: +```javascript +import { Promise, reject, CancelToken } from 'creed'; +function fetch(opts, token) { + if (typeof opts == 'string') { + opts = { method: 'GET', url: opts }; + } + token = CancelToken.from(token) || CancelToken.never(); + return new Promise(resolve => { + const xhr = new XMLHttpRequest(); + const nocancelAndResolve = token.subscribeOrCall(r => { + xhr.abort(r); + }, resolve); + xhr.onload = () => nocancelAndResolve(fulfill(xhr.response)); + xhr.onerror = e => nocancelAndResolve(reject(e)); + xhr.open(opts.method, opts.url, true); + }, token); +} +``` + +## Combine tokens + +### .concat :: CancelToken r → CancelToken r → CancelToken r + +[Fantasy-land Semigroup](https://github.com/fantasyland/fantasy-land#semigroup). +Returns a new cancellation token that is requested when the earlier of the two is requested. + +### CancelToken.race :: Iterable (CancelToken r) → Race r + +type Race r = { add :: CancelToken r → ... → (), get :: () → CancelToken r } + +The function returns a `Race` object populated with the tokens from the iterable. + +* The `add` method appends one or more tokens to the collection +* The `get` method returns a `CancelToken` that is revoked with the reason of the first requested cancellation in the collection + +Once the resulting token is cancelled, further `add` calls don't have any effect. + +### CancelToken.pool :: Iterable (CancelToken r) → Pool r + +type Pool r = { add :: CancelToken r → ... → (), get :: () → CancelToken r } + +The function returns a `Pool` object populated with the tokens from the iterable. + +* The `add` method appends one or more tokens to the collection +* The `get` method returns a `CancelToken` that is revoked with an array of the reasons once all (but at least one) tokens in the collection have requested cancellation + +Once the resulting token is cancelled, further `add` calls don't have any effect. + +### CancelToken.reference :: [CancelToken r] → Reference r + +type Reference r = { set :: [CancelToken r] → (), get :: () → CancelToken r } + +The function returns a `Reference` object storing the token (or nothing) from the argument + +* The `set` method puts a token or nothing (`null`, `undefined`) in the reference +* The `get` method returns a `CancelToken` that is revoked with the reason of the current reference once cancellation is requested + +Once the resulting token is cancelled, further `set` calls are forbidden. From 65b8c69564d292415424d66014df901bc569e78f Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 22 Jul 2016 14:04:07 +0200 Subject: [PATCH 27/28] update cancellation terminology and include link to official proposal --- README.md | 4 ++-- cancellation.md | 32 +++++++++++++++++++------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 534fcc9..18e08f5 100644 --- a/README.md +++ b/README.md @@ -534,7 +534,7 @@ fulfill(123).concat(fulfill(456)) Returns a promise equivalent to the receiver, but with the token associated to it. Equivalent to `.then(null, null, token)`. Essentially the cancellation is raced against the resolution. -Preference is given to the former, it always returns a cancelled promise if the token is already revoked. +Preference is given to the former, it always returns a cancelled promise if the token is already cancelled. ### .trifurcate :: Promise e a → (a → b|Thenable e b) → (e → b|Thenable e b) → (e → b|Thenable e b) → Promise e b @@ -561,7 +561,7 @@ fetch(…).untilCancel(token) // better: fetch(…, token) ### .finally :: Promise e a → (Promise e a → b|Thenable e b) → Promise e a -Runs the function when the promise settles or its associated token is revoked. +Runs the function when the promise settles or its associated token is cancelled. The resolution is not transformed, the callback result is awaited but ignored, unless it rejects. The returned promise has no cancellation token associated to it. diff --git a/cancellation.md b/cancellation.md index 35cf2f9..334a7c3 100644 --- a/cancellation.md +++ b/cancellation.md @@ -1,20 +1,26 @@ Creed features cancellation with a cancellation token based approach. +It is modelled after [this promise cancellation proposal](https://github.com/bergus/promise-cancellation), +altough minor discrepancies might be possible (if you find anything, please report a bug). + # Terminology 1. A **cancellation token** (**`CancelToken`**) is an object with methods for determining whether and when an operation should be cancelled. -2. A **revoked token** is a `CancelToken` that is in the cancelled state, denoting that the result of an operation is no longer of interest. -3. The cancellation can be **requested** by the issuer of the `CancelToken`, thereby revoking it. +2. The cancellation can be **requested** by the issuer of the `CancelToken`, denoting that the result of an operation is no longer of interest + and that the operation should be terminated if applicable. +3. A **cancelled token** is a `CancelToken` that represents a requested cancellation 4. A **cancellation reason** is a value used to request a cancellation and reject the respective promises. 5. One `CancelToken` might be **associated** with a promise. -6. A **cancelled promise** is a promise that got rejected because its associated token has been revoked. -7. A **cancelled callback** is an `onFulfilled` or `onRejected` handler whose corresponding cancellation token has been revoked. It might be considered an **unregistered** or **ignored** callback. +6. A **cancelled promise** is a promise that got rejected because its cancellation was requested through its associated token +7. The **corresponding** cancellation token of a handler is the associated token of the promise that the handler is meant to resolve +8. A **cancelled callback** is an `onFulfilled` or `onRejected` handler whose corresponding cancellation token has been cancelled. + It might be considered an **unregistered** or **ignored** callback. # Cancelling… Cancellation allows you to stop asynchronous operations built with promises. Use cases might both be in programmatical cancellation, where your program stops doing things after e.g. a timeout has expired or another operation has finished earlier, and in interactive cancellation, where a user triggers the stop through input methods. -Operations that are supposed to be stoppable must support this explicitly. It is not desired that anyone who holds a promise can cancel the operation that computes the result, therefore the invoker of the operation has to pass in a cancellation token that only he can revoke to request the cancellation. +Operations that are supposed to be stoppable must support this explicitly. It is not desired that anyone who holds a promise can cancel the operation that computes the result, therefore the invoker of the operation has to pass in a cancellation token that only he can request the cancellation. Passing around this capability explicitly can be a bit verbose at times, but everything else is done by Creed for you. ## …Promises @@ -69,7 +75,7 @@ const q = delay(4000).chain(x => { console.log('never executed'); return delay(1000, 'result'); }, token); -// the token being revoked prevents the inner delay from ever being created, and we get +// the token being cancelled prevents the inner delay from ever being created, and we get q.then(x => console.log(x), e => console.log(e)); //=> 'over' after 3 seconds ``` @@ -186,7 +192,7 @@ try { conn.close(); } ``` -where the connection is always closed, even when the `token` is revoked during the query. +where the connection is always closed, even when the `token` is cancelled during the query. It does also make it possible to react specifically to cancellation during a strand of execution in a coroutine: ```javascript @@ -240,7 +246,7 @@ Creates a cancellation token that is never requested, completing the [Fantasy-la ### .requested :: CancelToken r → boolean -Synchronously determines whether the token is revoked. +Synchronously determines whether the cancellation has been requested. ```javascript const { token, cancel } = CancelToken.source(); console.log(token.requested); //=> false @@ -265,7 +271,7 @@ cancel('reason'); Transforms the token's cancellation reason by applying the function to it. Returns a promise for the transformed result. The callback is invoked synchronously from a cancellation request, returning the promise also to the canceller. -If the token is already revoked, the callback is invoked asynchronously. +If the token is already cancelled, the callback is invoked asynchronously. ```javascript const { token, cancel } = CancelToken.source(); const p = token.subscribe(r => r + ' accepted'); @@ -276,7 +282,7 @@ p.then(x => console.log(x)); //=> reason accepted ### .subscribeOrCall :: CancelToken r → (r → a|Thenable e a) [→ (...* → b)] → (...* → [b]) -Subscribes the callback to be cancelled synchronously from a cancellation request or asynchronously when the token is already revoked. +Subscribes the callback to be cancelled synchronously from a cancellation request or asynchronously when the token is already cancelled. Returns a function that unsubscribes the callback. Unless the callback has already been executed, if the optional second parameter is a function it will be invoked at most once with the unsubscription arguments. @@ -326,7 +332,7 @@ type Race r = { add :: CancelToken r → ... → (), get :: () → Canc The function returns a `Race` object populated with the tokens from the iterable. * The `add` method appends one or more tokens to the collection -* The `get` method returns a `CancelToken` that is revoked with the reason of the first requested cancellation in the collection +* The `get` method returns a `CancelToken` that is cancelled with the reason of the first requested cancellation in the collection Once the resulting token is cancelled, further `add` calls don't have any effect. @@ -337,7 +343,7 @@ type Pool r = { add :: CancelToken r → ... → (), get :: () → Canc The function returns a `Pool` object populated with the tokens from the iterable. * The `add` method appends one or more tokens to the collection -* The `get` method returns a `CancelToken` that is revoked with an array of the reasons once all (but at least one) tokens in the collection have requested cancellation +* The `get` method returns a `CancelToken` that is cancelled with an array of the reasons once all (but at least one) tokens in the collection have requested cancellation Once the resulting token is cancelled, further `add` calls don't have any effect. @@ -348,6 +354,6 @@ type Reference r = { set :: [CancelToken r] → (), get :: () → CancelTo The function returns a `Reference` object storing the token (or nothing) from the argument * The `set` method puts a token or nothing (`null`, `undefined`) in the reference -* The `get` method returns a `CancelToken` that is revoked with the reason of the current reference once cancellation is requested +* The `get` method returns a `CancelToken` that is cancelled with the reason of the current reference once cancellation is requested Once the resulting token is cancelled, further `set` calls are forbidden. From 911c1d0462fc0468cd377b0f43c48ba0e88f056a Mon Sep 17 00:00:00 2001 From: Bergi Date: Fri, 29 Jul 2016 06:55:02 +0200 Subject: [PATCH 28/28] Make token subscriptions asynchronous Fixes #1. * Token composition is still synchronous, as it requires some trickery with returning flat arrays of subscription results. * Subscriptions, finally, trifurcate, and coroutine cancellation all behave asynchronous now, which required some test rewrites. * Action no longer auto-subscribes, as that may cause cancel to be called synchronously before subclass constructors are done with initialisation --- src/Action.js | 31 ++++------- src/CancelToken.js | 69 ++++++++++++++----------- src/Promise.js | 44 ++++++++++++++-- src/chain.js | 2 +- src/coroutine.js | 46 +++++++---------- src/delay.js | 2 +- src/finally.js | 93 +++++++++++++-------------------- src/iterable.js | 2 +- src/map.js | 2 +- src/subscribe.js | 15 +++--- src/then.js | 2 +- src/timeout.js | 2 +- src/trifurcate.js | 11 ++-- test/CancelToken-test.js | 108 +++++++++++++++++++++------------------ test/coroutine-test.js | 15 ++++-- test/finally-test.js | 46 ++++++++++------- test/lib/test-util.js | 2 - 17 files changed, 263 insertions(+), 229 deletions(-) diff --git a/src/Action.js b/src/Action.js index ce969a3..7d16424 100644 --- a/src/Action.js +++ b/src/Action.js @@ -1,17 +1,11 @@ import { noop } from './util' -import { Future, reject } from './Promise' +import { reject } from './Promise' export default class Action { constructor (promise) { // the Future which this Action tries to resolve // when null, the action is cancelled and won't be executed this.promise = promise - - const token = promise.token - if (token != null) { - // assert: !token.requested - token._subscribe(this) - } } destroy () { @@ -38,7 +32,7 @@ export default class Action { // default onCancelled action cancelled (p) { - reject(p.value)._runAction(this) + reject(p.near().value)._runAction(this) } // when this.promise is to be settled (possible having awaited the result) @@ -58,7 +52,6 @@ export default class Action { } const sentinel = noop // Symbol('currently executing') -const empty = [] export class CancellableAction extends Action { constructor (f, promise) { @@ -73,17 +66,16 @@ export class CancellableAction extends Action { this.f = null } - cancel (p) { + cancel (results) { if (this.promise._isResolved()) { // promise checks for cancellation itself - if (this.f === sentinel) { - this.destroy() - this.promise = new Future() // allow to relay feedback to the cancel() call - return this.promise // TODO: really useful? leaking callback results is weird - } else { + if (this.f !== sentinel) { // not currently running this.destroy() - return empty } + // otherwise keep the cancelled .promise so that it stays usable in handle() + // and ignores whatever is done with the f() result + return true } + return false } fulfilled (p) { @@ -95,20 +87,17 @@ export class CancellableAction extends Action { } tryCall (f, x) { - const original = this.promise this.f = sentinel let result try { result = f(x) } catch (e) { this.f = null - const uncancelled = this.promise === original this.end()._reject(e) - return uncancelled + return } this.f = null - this.handle(result) - return this.promise === original + return this.handle(result) } handle (p) { diff --git a/src/CancelToken.js b/src/CancelToken.js index 0e2835d..6c372fd 100644 --- a/src/CancelToken.js +++ b/src/CancelToken.js @@ -23,21 +23,38 @@ export default class CancelToken { } __cancel (p) { this._cancelled = true + if (this.length) { + taskQueue.add(this) // needs to be called before __become + } if (this.promise !== void 0) { this.promise.__become(p) } else { - p.token = this // TODO ugly but necessary? + // p.token = this no more necessary this.promise = p } - return this.run() + return this._runSync([]) + } + _runSync (results) { + // let j = 0; + for (let i = 0; i < this.length; ++i) { + if (this[i] && this[i].promise) { // not already destroyed + this[i].cancel(results, this.promise) + // if (this[i].promise) { + // this[j++] = this[i] + // } + } + // if (j < i) + // this[i] = void 0 + // } + } + // this.length = j; + return results } run () { - /* eslint complexity:[2,4] */ - const result = [] const l = this.length for (let i = 0; i < l; ++i) { if (this[i] && this[i].promise) { // not already destroyed - this._runAction(this[i], result) + this[i].cancelled(this.promise) } this[i] = void 0 } @@ -46,23 +63,13 @@ export default class CancelToken { } else { taskQueue.add(this) } - return result - } - _runAction (action, results) { - const res = action.cancel(this.promise) - if (res != null) { - if (Array.isArray(res)) { - if (res.length > 0) { - results.push(...res) - } - } else { - results.push(res) - } - } } _subscribe (action) { - if (this.requested && this.length === 0) { - taskQueue.add(this) + if (this.requested) { + action.cancel(null, this) + if (this.length === 0) { + taskQueue.add(this) + } } this[this.length++] = action } @@ -87,7 +94,7 @@ export default class CancelToken { } } if (action) { // when not found - action.destroy() // at least mark explictly as empty + action.destroy() // at least mark explicitly as empty } } subscribe (fn, token) { @@ -169,7 +176,8 @@ class CancelTokenCombinator { // implements cancel parts of Action destroy () { // possibly called when unsubscribed from a token } - // abstract cancel (p) {} + cancelled (p) {} + // abstract cancel (res, p) {} // abstract _testRequested () {} get () { return this.promise @@ -182,7 +190,7 @@ class CancelTokenRace extends CancelTokenCombinator { this.tokens = [] if (tokens) this.add(...tokens) } - cancel (p) { + cancel (results, p) { /* istanbul ignore if */ if (this.tokens == null) return // when called after been unsubscribed but not destroyed // assert: !this.promise._cancelled @@ -191,7 +199,8 @@ class CancelTokenRace extends CancelTokenCombinator { t._unsubscribe(this) } this.tokens = null - return this.promise.__cancel(p) + const res = this.promise.__cancel(p) + if (results) results.push(...res) } _testRequested () { return this.tokens.some(t => t.requested) @@ -205,7 +214,7 @@ class CancelTokenRace extends CancelTokenCombinator { continue } if (t.requested) { - this.cancel(t.getCancelled()) + this.cancel(null, t.getCancelled()) break } else { this.tokens.push(t) @@ -222,10 +231,11 @@ class CancelTokenPool extends CancelTokenCombinator { this.count = 0 if (tokens) this.add(...tokens) } - cancel (p) { + cancel (results, p) { // assert: !this.promise._cancelled this.count-- - return this._check() + const res = this._check() + if (results && res) results.push(...res) } _testRequested () { return this.tokens.length > 0 && this.tokens.every(t => t.requested) @@ -265,11 +275,12 @@ export class CancelTokenReference extends CancelTokenCombinator { super() this.curToken = cur } - cancel (p) { + cancel (results, p) { /* istanbul ignore if */ if (this.curToken == null || this.curToken.getCancelled() !== p) return // when called from an oldToken // assert: !this.promise._cancelled - return this.promise.__cancel(p) + const res = this.promise.__cancel(p) + if (results) results.push(...res) } _testRequested () { return this.curToken != null && this.curToken.requested diff --git a/src/Promise.js b/src/Promise.js index 0ad3798..f6b5134 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -44,6 +44,10 @@ class Core { toString () { return '[object ' + this.inspect() + ']' } + + _whenToken (action) { + return action + } } // data Promise e a where @@ -120,14 +124,16 @@ export class Future extends Core { if (p.token.requested) { return p.token.getCancelled() } - this._runAction(new Action(p)) + const put = new Action(p) + token._subscribe(put) + this._runAction(put) return p } // finally :: Promise e a -> (Promise e a -> ()) -> Promise e a finally (f) { const n = this.near() - return n === this ? fin(f, this, new Future()) : n.finally(f) + return n === this ? fin(f, this, new Future(this.token)) : n.finally(f) } // trifurcate :: Promise e a -> (a -> b) -> (e -> b) -> (e -> b) -> Promise e b @@ -176,6 +182,14 @@ export class Future extends Core { } } + _whenToken (action) { + if (this.token != null) { + // assert: !this.token.requested + this.token._subscribe(action) + } + return action + } + _resolve (x, cancelAction) { if (this._isResolved()) { return // TODO: still resolve thenables when cancelled? @@ -204,7 +218,7 @@ export class Future extends Core { } else if ((state & PENDING) > 0 && this.token !== p.token) { this.ref = this // reuse cancelAction - do not .end() it here - p._runAction(cancelAction || new Action(this)) + p._runAction(cancelAction || this._whenToken(new Action(this))) return } } @@ -405,6 +419,10 @@ class Rejected extends Core { // Cancelled :: Error e => e -> Promise e a // A promise whose value was invalidated and cannot be known class Cancelled extends Rejected { + finally (f) { + return fin(f, this, this) + } + trifurcate (f, r, c) { return trifurcate(undefined, undefined, c, this, new Future()) } @@ -423,9 +441,25 @@ class Cancelled extends Rejected { return token != null && token.requested ? token.getCancelled() : reject(this.value) } + _isResolved () { + // called by Final::cancel + return true + } + _runAction (action) { // assert: action.promise != null - action.cancelled(this) + action.rejected(this) + } + + _whenToken (action) { + // behaves as if there was a .token + action.cancel(null, this) + taskQueue.add(new Continuation(action, { + _runAction: action => { + action.cancelled(this) + } + })) + return action } } @@ -559,6 +593,7 @@ export function future (token) { } } let put = new Action(promise) + promise.token._subscribe(put) return { promise, resolve (x) { @@ -573,6 +608,7 @@ export function future (token) { export function makeResolvers (promise) { if (promise.token != null) { let put = new Action(promise) + promise.token._subscribe(put) return { resolve (x) { if (put == null || put.promise == null) return diff --git a/src/chain.js b/src/chain.js index 60b97ad..07f6dff 100644 --- a/src/chain.js +++ b/src/chain.js @@ -5,7 +5,7 @@ export default function chain (f, p, promise) { if (promise.token != null && promise.token.requested) { return promise.token.getCancelled() } - p._when(new Chain(f, promise)) + p._when(promise._whenToken(new Chain(f, promise))) return promise } diff --git a/src/coroutine.js b/src/coroutine.js index bb6ffdc..55b5130 100644 --- a/src/coroutine.js +++ b/src/coroutine.js @@ -31,7 +31,7 @@ Object.defineProperty(coroutine, 'cancel', { function runGenerator (generator) { const swappable = CancelToken.reference(null) const promise = new Future(swappable.get()) - new Coroutine(generator, promise, swappable).run() + promise._whenToken(new Coroutine(generator, promise, swappable)).run() // taskQueue.add(new Coroutine(generator, promise, swappable)) return promise } @@ -39,7 +39,7 @@ function runGenerator (generator) { class Coroutine extends Action { constructor (generator, promise, ref) { super(promise) - // the generator that is driven. After cancellation, reference to cleanup coroutine + // the generator that is driven. Empty after cancellation this.generator = generator // a CancelTokenReference this.tokenref = ref @@ -50,28 +50,34 @@ class Coroutine extends Action { } fulfilled (ref) { + if (this.generator == null) return this.step(this.generator.next, ref.value) } rejected (ref) { + if (this.generator == null) return false this.step(this.generator.throw, ref.value) return true } - cancel (p) { + cancel (results) { /* istanbul ignore else */ if (this.promise._isResolved()) { // promise checks for cancellation itself - // assert: p === this.promise.token.getCancelled() - this.promise = null const res = new Future() - this.generator = new Coroutine(this.generator, res, p.near().value) - if (stack.indexOf(this) < 0) { - this.resumeCancel() - } - return res + this.promise = new Coroutine(this.generator, res, null) // not cancellable + this.generator = null + this.tokenref = null + if (results) results.push(res) } } + cancelled (p) { + const cancelRoutine = this.promise + this.promise = null + const reason = p.near().value + cancelRoutine.step(cancelRoutine.generator.return, reason) + } + step (f, x) { /* eslint complexity:[2,5] */ let result @@ -83,37 +89,23 @@ class Coroutine extends Action { } finally { stack.pop() // assert: === this } - if (this.promise) { + if (this.generator) { // not cancelled during execution const res = resolve(result.value, this.promise.token) // TODO optimise token? if (result.done) { this.put(res) } else { res._runAction(this) } - } else { // cancelled during execution - // ignoring result.done and result.value - // if done, one would only need to resolve the initialised promise and not call return() - this.resumeCancel() } } - resumeCancel () { - const cancelRoutine = this.generator - this.generator = null - const reason = cancelRoutine.tokenref - cancelRoutine.tokenref = null // not cancellable - // assert: reason === this.tokenref.get().getCancelled().value - this.tokenref = null - cancelRoutine.step(cancelRoutine.generator.return, reason) - } - setToken (t) { - if (this.tokenref == null) throw new SyntaxError('coroutine.cancel is only available until cancellation') + if (this.tokenref == null) throw new ReferenceError('coroutine.cancel is only available until cancellation') this.tokenref.set(t) } getToken () { - if (this.tokenref == null) throw new SyntaxError('coroutine.cancel is only available until cancellation') + if (this.tokenref == null) throw new ReferenceError('coroutine.cancel is only available until cancellation') return this.tokenref.get() } } diff --git a/src/delay.js b/src/delay.js index 8e55d8f..f41be19 100644 --- a/src/delay.js +++ b/src/delay.js @@ -1,7 +1,7 @@ import Action from './Action' export default function delay (ms, p, promise) { - p._runAction(new Delay(ms, promise)) + p._runAction(promise._whenToken(new Delay(ms, promise))) return promise } diff --git a/src/finally.js b/src/finally.js index 0bdd513..d81c71c 100644 --- a/src/finally.js +++ b/src/finally.js @@ -1,35 +1,29 @@ -import { resolve } from './Promise' -import { isRejected, isFulfilled } from './inspect' +import { resolve, Future } from './Promise' +// import { isFulfilled, isRejected } from './inspect' import { CancellableAction } from './Action' export default function _finally (f, p, promise) { - // assert: promise.token == null + // assert: promise.token == p.token if (typeof f !== 'function') throw new TypeError('finally does require a callback function') - p._when(new Final(f, p.token, promise)) + p._when(p._whenToken(new Final(f, promise))) return promise } class Final extends CancellableAction { - constructor (f, t, promise) { - super(f, promise) - this.token = t - if (t != null) { - t._subscribe(this) - } - } - - /* istanbul ignore next */ - destroy () { // possibly called when unsubscribed from the token - this.token = null + destroy () { + this.promise = null + // don't destroy f } - cancel (p) { - /* istanbul ignore if */ - if (this.token == null) return - this.token = null - const promise = this.tryFin(p) - this.promise = null // prevent cancelled from running - return promise + cancel (results) { + super.cancel(null) // cancel the final promise + if (typeof this.f === 'function') { // yet to be run or currently running + // assert: this.f === sentinel || this.promise = null + this.promise = new Future() // create new promise for the cancel result + if (results) results.push(this.promise) + } else { // f already ran, .f holds the original now, .promise was the final promise + // do anything to the f() result? + } } fulfilled (p) { @@ -37,53 +31,38 @@ class Final extends CancellableAction { } rejected (p) { - return this.settled(p, p) + return this.settled(p, null) } - settled (p, res) { + cancelled (p) { + this.runFin(p.near(), null) + } + + settled (p, orig) { if (typeof this.f === 'function') { // f is the callback - const token = this.token - if (token) { - token._unsubscribe(this) - this.token = null - } - this.tryFin(p) + this.runFin(p, p) return true } else { // f held the original result - this.promise.__become(res) - this.promise = this.f = null + this.put(orig == null ? p : orig) + this.f = null return false } } - tryFin (p) { - /* eslint complexity:[2,5] */ - const orig = this.promise - if (!this.tryCall(this.f, p)) { - // assert: orig !== this.promise - // assert: !isRejeced(this.promise) - if (isFulfilled(this.promise)) { - orig._become(p) - } else { - this.f = p - this.promise._runAction(this) - this.promise = orig - } - return this.promise + runFin (p, orig) { + const res = this.tryCall(this.f, p) + if (res !== undefined) { // f returned a promise to wait for + // if (isFulfilled(res)) return this.put(p) + // if (isRejected(res)) return this.put(res) + this.f = orig // reuse property to store eventual result + res._runAction(this) + } else if (this.promise) { // f returned nothing and didn't throw + this.put(p) } - return orig } handle (result) { - const p = resolve(result) - if (isRejected(p)) { - this.promise._become(p) - } else { - this.promise = p - } - } - - end () { - return this.promise + if (result == null) return + return resolve(result) } } diff --git a/src/iterable.js b/src/iterable.js index 1e201f3..da038ab 100644 --- a/src/iterable.js +++ b/src/iterable.js @@ -74,7 +74,7 @@ function handleItem (handler, x, i, promise) { } else if (isRejected(p)) { handler.rejectAt(p, i, promise) } else { - p._runAction(new Indexed(handler, i, promise)) + p._runAction(promise._whenToken(new Indexed(handler, i, promise))) } } diff --git a/src/map.js b/src/map.js index 5640ade..0fd8e17 100644 --- a/src/map.js +++ b/src/map.js @@ -4,7 +4,7 @@ export default function map (f, p, promise) { if (promise.token != null && promise.token.requested) { return promise.token.getCancelled() } - p._when(new Map(f, promise)) + p._when(promise._whenToken(new Map(f, promise))) return promise } diff --git a/src/subscribe.js b/src/subscribe.js index b39c984..d70ee70 100644 --- a/src/subscribe.js +++ b/src/subscribe.js @@ -26,16 +26,19 @@ export function subscribeOrCall (f, g, t, promise) { } class Subscription extends CancellableAction { - cancel (p) { + cancel (results) { /* eslint complexity:[2,4] */ const promise = this.promise - if (promise.token != null) { - const res = super.cancel(p) - if (res != null) { - return res + if (promise.token != null) { // possibly called from promise.token + if (super.cancel()) { // if promise is cancelled + return } } + // otherwise called from standalone token + if (results) results.push(this.promise) + } + + cancelled (p) { this.tryCall(this.f, p.near().value) - return promise } } diff --git a/src/then.js b/src/then.js index b7a47c6..d65bc3c 100644 --- a/src/then.js +++ b/src/then.js @@ -4,7 +4,7 @@ export default function then (f, r, p, promise) { if (promise.token != null && promise.token.requested) { return promise.token.getCancelled() } - p._when(new Then(f, r, promise)) + p._when(promise._whenToken(new Then(f, r, promise))) return promise } diff --git a/src/timeout.js b/src/timeout.js index f1094f2..b8a05bc 100644 --- a/src/timeout.js +++ b/src/timeout.js @@ -3,7 +3,7 @@ import TimeoutError from './TimeoutError' export default function timeout (ms, p, promise) { const timer = setTimeout(rejectOnTimeout, ms, promise) - p._runAction(new Timeout(timer, promise)) + p._runAction(promise._whenToken(new Timeout(timer, promise))) return promise } diff --git a/src/trifurcate.js b/src/trifurcate.js index 9a526ea..764462c 100644 --- a/src/trifurcate.js +++ b/src/trifurcate.js @@ -2,7 +2,7 @@ import { CancellableAction } from './Action' export default function trifurcate (f, r, c, p, promise) { // assert: promise.token == null - p._when(new Trifurcation(f, r, c, promise)) + p._when(p._whenToken(new Trifurcation(f, r, c, promise))) return promise } @@ -13,6 +13,10 @@ class Trifurcation extends CancellableAction { this.c = c } + cancel (res) { + // assert: cancelled() is called later, before rejected() is called + } + fulfilled (p) { this.runTee(this.f, p) } @@ -23,10 +27,11 @@ class Trifurcation extends CancellableAction { cancelled (p) { if (typeof this.c !== 'function') { - this.end()._reject(p.value) + this.end()._reject(p.near().value) } else { - this.runTee(this.c, p) + this.runTee(this.c, p.near()) } + // assert: this.promise == null, so that rejected won't run } runTee (f, p) { diff --git a/test/CancelToken-test.js b/test/CancelToken-test.js index 120956d..6675a11 100644 --- a/test/CancelToken-test.js +++ b/test/CancelToken-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { CancelToken, isCancelled, isPending, getReason, future } from '../src/main' +import { CancelToken, isCancelled, isPending, getReason, future, all } from '../src/main' import { assertSame, FakeCancelAction } from './lib/test-util' import assert from 'assert' @@ -106,7 +106,7 @@ describe('CancelToken', function () { it('should synchronously run subscriptions', () => { const {token, cancel} = CancelToken.source() const r = {} - const action = new FakeCancelAction({}, p => assert.strictEqual(getReason(p), r)) + const action = new FakeCancelAction({}) token._subscribe(action) assert(!action.isCancelled) cancel(r) @@ -172,47 +172,66 @@ describe('CancelToken', function () { it('should run subscriptions when already requested', () => { const {token, cancel} = CancelToken.source() const {resolve, promise} = future() - cancel() - token._subscribe(new FakeCancelAction({}, p => resolve(getReason(p)))) - return promise + const r = {} + const action = new FakeCancelAction({}) + cancel(r) + token._subscribe(action) + assert(action.isCancelled) }) }) describe('subscribe()', () => { - it('should synchronously call subscriptions', () => { + it('should asynchronously call subscriptions', () => { + const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() + token.subscribe(resolve) + cancel() + assert(isPending(promise)) + return promise + }) + + it('should return a promise for the result', () => { + const {token, cancel} = CancelToken.source() + const expected = {} + const p = token.subscribe(() => expected) + assert.strictEqual(cancel()[0], p) + return p.then(x => { + assert.strictEqual(x, expected) + }) + }) + + it('should call subscriptions with the reason', () => { const {token, cancel} = CancelToken.source() const r = {} - let isCalled = false - token.subscribe(e => { - isCalled = true + const p = token.subscribe(e => { assert.strictEqual(e, r) }) - assert(!isCalled) cancel(r) - assert(isCalled) + return p }) it('should not call subscriptions multiple times', () => { const {token, cancel} = CancelToken.source() let calls = 0 - token.subscribe(() => { + const p = token.subscribe(() => { calls++ + assert.strictEqual(calls, 1) }) assert.strictEqual(calls, 0) cancel() cancel() - assert.strictEqual(calls, 1) + return p }) it('should ignore exceptions thrown by subscriptions', () => { const {token, cancel} = CancelToken.source() let isCalled = false token.subscribe(() => { throw new Error() }) - token.subscribe(e => { + const p = token.subscribe(e => { isCalled = true }) cancel() - assert(isCalled) + return p.then(() => assert(isCalled)) }) it('should call subscriptions when already requested', () => { @@ -226,26 +245,17 @@ describe('CancelToken', function () { it('should call subscriptions in order', () => { const {token, cancel} = CancelToken.source() let s = 0 - token.subscribe(() => { + const a = token.subscribe(() => { assert.strictEqual(s, 0) s = 1 }) - token.subscribe(() => { + const b = token.subscribe(() => { assert.strictEqual(s, 1) s = 2 }) cancel() - assert.strictEqual(s, 2) - }) - - it('should return a promise for the result', () => { - const {token, cancel} = CancelToken.source() - const expected = {} - const p = token.subscribe(() => expected) - assert.strictEqual(cancel()[0], p) - return p.then(x => { - assert.strictEqual(x, expected) - }) + assert.strictEqual(s, 0) + return all([a, b]).then(() => assert.strictEqual(s, 2)) }) it('should behave nearly like getCancelled().catch()', () => { @@ -263,9 +273,7 @@ describe('CancelToken', function () { const expected = {} const p = token.subscribe(() => { throw expected }) assert.strictEqual(cancel()[0], p) - return p.then(assert.ifError, x => { - assert.strictEqual(x, expected) - }) + return p.then(assert.ifError, x => assert.strictEqual(x, expected)) }) it('should call subscriptions before the token is cancelled', () => { @@ -273,23 +281,18 @@ describe('CancelToken', function () { const expected = {} const p = token.subscribe(() => expected, CancelToken.empty()) cancel() - return p.then(x => { - assert.strictEqual(x, expected) - }) + return p.then(x => assert.strictEqual(x, expected)) }) it('should not call subscriptions when the token is cancelled', () => { const a = CancelToken.source() const b = CancelToken.source() - let called = false const p = a.token.subscribe(() => { - called = true + throw new Error("should not be called") }, b.token) assert.strictEqual(p.token, b.token) b.cancel() - assert(!called) a.cancel() - assert(!called) return assertSame(p, b.token.getCancelled()) }) @@ -316,25 +319,26 @@ describe('CancelToken', function () { const expected = {} const p = token.subscribe(() => token.subscribe(() => expected)) assert.strictEqual(cancel().length, 1) - return p.then(x => { - assert.strictEqual(x, expected) - }) + return p.then(x => assert.strictEqual(x, expected)) }) }) describe('subscribeOrCall()', () => { it('should invoke f if the token is cancelled before the call', () => { const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() let called = 0 - const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2 }) + const call = token.subscribeOrCall(() => { called |= 1; resolve() }, () => { called |= 2 }) assert.strictEqual(called, 0) cancel() - assert.strictEqual(called, 1) - call() - assert.strictEqual(called, 1) + return promise.then(() => { + assert.strictEqual(called, 1) + call() + assert.strictEqual(called, 1) + }) }) - it('should invoke g if the call happens before the cancellation', () => { + it('should invoke g immediately if the call happens before the cancellation', () => { const {token, cancel} = CancelToken.source() let called = 0 const call = token.subscribeOrCall(() => { called |= 1 }, () => { called |= 2 }) @@ -342,16 +346,17 @@ describe('CancelToken', function () { call() assert.strictEqual(called, 2) cancel() - assert.strictEqual(called, 2) + return token.subscribe(() => assert.strictEqual(called, 2)) }) it('should only invoke f if the call happens during the cancellation', () => { const {token, cancel} = CancelToken.source() + const {resolve, promise} = future() let called = 0 - const call = token.subscribeOrCall(() => { called |= 1; call() }, () => { called |= 2 }) + const call = token.subscribeOrCall(() => { called |= 1; call(); resolve() }, () => { called |= 2 }) assert.strictEqual(called, 0) cancel() - assert.strictEqual(called, 1) + return promise.then(() => assert.strictEqual(called, 1)) }) it('should only invoke g if the token is cancelled during the call', () => { @@ -361,17 +366,18 @@ describe('CancelToken', function () { assert.strictEqual(called, 0) call() assert.strictEqual(called, 2) + return token.subscribe(() => assert.strictEqual(called, 2)) }) it('should cope with undefined g', () => { const {token, cancel} = CancelToken.source() let called = 0 - const call = token.subscribeOrCall(() => { called = 0 }) + const call = token.subscribeOrCall(() => { called |= 1 }) assert.strictEqual(called, 0) call() assert.strictEqual(called, 0) cancel() - assert.strictEqual(called, 0) + return token.subscribe(() => assert.strictEqual(called, 0)) }) it('should throw exceptions from g', () => { diff --git a/test/coroutine-test.js b/test/coroutine-test.js index 94374bd..d260868 100644 --- a/test/coroutine-test.js +++ b/test/coroutine-test.js @@ -1,5 +1,5 @@ import { describe, it } from 'mocha' -import { coroutine, fulfill, reject, delay, isCancelled, CancelToken } from '../src/main' +import { coroutine, fulfill, reject, delay, all, isCancelled, CancelToken } from '../src/main' import { assertSame } from './lib/test-util' import assert from 'assert' @@ -81,7 +81,10 @@ describe('coroutine', function () { }) const {token, cancel} = CancelToken.source() f(token) - return delay(3, {}).then(cancel).then(() => { + return delay(3, {}) + .then(cancel) + .then(all) + .then(() => { assert(!executedT, 'after yield') assert(!executedC, 'catch block') assert(executedF, 'finally block') @@ -172,8 +175,10 @@ describe('coroutine', function () { const {token, cancel} = CancelToken.source() const p = f(token) return delay(15).then(() => { - cancel({}) + const res = cancel({}) assert(isCancelled(p)) + return all(res) + }).then(() => { assert.strictEqual(counter, 0) }) }) @@ -217,8 +222,8 @@ describe('coroutine', function () { } finally { try { assert(isCancelled(p)) - assert.throws(() => coroutine.cancel, SyntaxError) - assert.throws(() => { coroutine.cancel = null }, SyntaxError) + assert.throws(() => coroutine.cancel, ReferenceError) + assert.throws(() => { coroutine.cancel = null }, ReferenceError) } catch (e) { err = e } diff --git a/test/finally-test.js b/test/finally-test.js index c2c0992..4d82bdf 100644 --- a/test/finally-test.js +++ b/test/finally-test.js @@ -59,7 +59,7 @@ describe('finally', () => { const {promise, resolve} = future(CancelToken.empty()) const p = promise.finally(() => { called = true - }).then(assert.ifError, () => { + }).then(() => { assert(called) }) resolve(fulfill()) @@ -72,24 +72,35 @@ describe('finally', () => { resolve(fulfill()) return promise.finally(() => { called = true - }).then(assert.ifError, () => { + }).then(() => { + assert(called) + }) + }) + + it('should call f for already cancelled future', () => { + let called = false + const {token, cancel} = CancelToken.source() + cancel() + const {promise} = future(token) + return promise.finally(() => { + called = true + }).trifurcate(assert.ifError, assert.ifError, () => { assert(called) }) }) describe('cancel', () => { - it('should call f synchronously', () => { + it('should call f asynchronously', () => { let called = false const {token, cancel} = CancelToken.source() const p = delay(1, null, token).finally(() => { called = true - }).then(assert.ifError, () => { - assert(called) }) - assert(!called) cancel() - assert(called) - return p + assert(!called) + return p.trifurcate(assert.ifError, assert.ifError, () => { + assert(called) + }) }) it('should return fulfilled callback result', () => { @@ -98,7 +109,7 @@ describe('finally', () => { const {token, cancel} = CancelToken.source() const p = delay(1, null, token).finally(() => { return expected - }).then(assert.ifError, e => { + }).trifurcate(assert.ifError, assert.ifError, e => { assert.strictEqual(e, reason) return assertSame(c[0], expected) }) @@ -111,23 +122,22 @@ describe('finally', () => { const {token, cancel} = CancelToken.source() const p = delay(1, null, token).finally(() => { throw expected - }).then(assert.ifError, e => { - assert.strictEqual(e, expected) + }).trifurcate(assert.ifError, assert.ifError, () => { return assertSame(c[0], reject(expected)) }) const c = cancel() return p }) - it('should do nothing during f call for fulfilled future', () => { - const expected = {} + it('should cancel result during f call for fulfilled future', () => { + const reason = {} const {token, cancel} = CancelToken.source() let c - return delay(1, fulfill(expected), token).finally(() => { - c = cancel({}) - }).then(x => { - assert.strictEqual(x, expected) - assert.strictEqual(c.length, 0) + return delay(1, null, token).finally(() => { + c = cancel(reason) + }).trifurcate(assert.ifError, assert.ifError, e => { + assert.strictEqual(e, reason) + assert.strictEqual(c.length, 1) }) }) }) diff --git a/test/lib/test-util.js b/test/lib/test-util.js index 4c49ec1..086f9ca 100644 --- a/test/lib/test-util.js +++ b/test/lib/test-util.js @@ -61,7 +61,6 @@ class ThrowingIterator { export class FakeCancelAction { constructor (promise, cb) { this.promise = promise - this.cb = cb this.isCancelled = 0 this.isDestroyed = 0 const token = promise.token @@ -77,7 +76,6 @@ export class FakeCancelAction { cancel (p) { this.isCancelled++ - if (typeof this.cb === 'function') this.cb(p) if (typeof this.promise._isResolved !== 'function' || this.promise._isResolved()) { this.destroy() }