Commit f8ce66af authored by Romain Courteaud's avatar Romain Courteaud

Adding promise cancel

Cancel allow to manually interrupt a promise execution.
The promise becomes rejected with a CancellationError.

var xhr, promise;
promise = RSVP.Promise(
  // Resolver function
  function (done, fail) {
    xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
  },
  // Canceller function
  function () {
    xhr.abort();
  },
)
promise.cancel();
parent dd67073e
import { EventTarget } from "./rsvp/events"; import { EventTarget } from "./rsvp/events";
import { CancellationError } from "./rsvp/cancellation_error";
import { Promise } from "./rsvp/promise"; import { Promise } from "./rsvp/promise";
import { denodeify } from "./rsvp/node"; import { denodeify } from "./rsvp/node";
import { all } from "./rsvp/all"; import { all } from "./rsvp/all";
...@@ -13,4 +14,4 @@ function configure(name, value) { ...@@ -13,4 +14,4 @@ function configure(name, value) {
config[name] = value; config[name] = value;
} }
export { Promise, EventTarget, all, hash, rethrow, defer, denodeify, configure, resolve, reject }; export { CancellationError, Promise, EventTarget, all, hash, rethrow, defer, denodeify, configure, resolve, reject };
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
function CancellationError(message) {
this.name = "cancel";
if ((message !== undefined) && (typeof message !== "string")) {
throw new TypeError('You must pass a string.');
}
this.message = message || "Default Message";
}
CancellationError.prototype = new Error();
CancellationError.prototype.constructor = CancellationError;
export { CancellationError };
import { config } from "./config"; import { config } from "./config";
import { EventTarget } from "./events"; import { EventTarget } from "./events";
import { CancellationError } from "./cancellation_error";
function objectOrFunction(x) { function objectOrFunction(x) {
return isFunction(x) || (typeof x === "object" && x !== null); return isFunction(x) || (typeof x === "object" && x !== null);
...@@ -9,7 +10,7 @@ function isFunction(x){ ...@@ -9,7 +10,7 @@ function isFunction(x){
return typeof x === "function"; return typeof x === "function";
} }
var Promise = function(resolver) { var Promise = function(resolver, canceller) {
var promise = this, var promise = this,
resolved = false; resolved = false;
...@@ -17,8 +18,13 @@ var Promise = function(resolver) { ...@@ -17,8 +18,13 @@ var Promise = function(resolver) {
throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor'); throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor');
} }
if ((canceller !== undefined) && (typeof canceller !== 'function')) {
throw new TypeError('You can only pass a canceller function' +
' as the second argument to the promise constructor');
}
if (!(promise instanceof Promise)) { if (!(promise instanceof Promise)) {
return new Promise(resolver); return new Promise(resolver, canceller);
} }
var resolvePromise = function(value) { var resolvePromise = function(value) {
...@@ -39,6 +45,22 @@ var Promise = function(resolver) { ...@@ -39,6 +45,22 @@ var Promise = function(resolver) {
this.on('error', onerror); this.on('error', onerror);
this.cancel = function () {
// For now, simply reject the promise and does not propagate the cancel
// to parent or children
if (resolved) { return; }
if (canceller !== undefined) {
try {
canceller();
} catch (e) {
rejectPromise(e);
return;
}
}
// Trigger cancel?
rejectPromise(new CancellationError());
};
try { try {
resolver(resolvePromise, rejectPromise); resolver(resolvePromise, rejectPromise);
} catch(e) { } catch(e) {
...@@ -93,7 +115,10 @@ Promise.prototype = { ...@@ -93,7 +115,10 @@ Promise.prototype = {
then: function(done, fail) { then: function(done, fail) {
this.off('error', onerror); this.off('error', onerror);
var thenPromise = new this.constructor(function() {}); var thenPromise = new this.constructor(function() {},
function () {
thenPromise.trigger('promise:cancelled', {});
});
if (this.isFulfilled) { if (this.isFulfilled) {
config.async(function(promise) { config.async(function(promise) {
...@@ -146,6 +171,9 @@ function handleThenable(promise, value) { ...@@ -146,6 +171,9 @@ function handleThenable(promise, value) {
then = value.then; then = value.then;
if (isFunction(then)) { if (isFunction(then)) {
promise.on('promise:cancelled', function(event) {
value.cancel();
});
then.call(value, function(val) { then.call(value, function(val) {
if (resolved) { return true; } if (resolved) { return true; }
resolved = true; resolved = true;
......
...@@ -65,9 +65,9 @@ describe("RSVP extensions", function() { ...@@ -65,9 +65,9 @@ describe("RSVP extensions", function() {
}); });
describe("Promise constructor", function() { describe("Promise constructor", function() {
it('should exist and have length 1', function() { it('should exist and have length 2', function() {
assert(RSVP.Promise); assert(RSVP.Promise);
assert.equal(RSVP.Promise.length, 1); assert.equal(RSVP.Promise.length, 2);
}); });
it('should fulfill if `resolve` is called with a value', function(done) { it('should fulfill if `resolve` is called with a value', function(done) {
...@@ -122,6 +122,17 @@ describe("RSVP extensions", function() { ...@@ -122,6 +122,17 @@ describe("RSVP extensions", function() {
}, TypeError); }, TypeError);
}); });
it('should throw a `TypeError` if the `canceller` is provided ' +
'and not a function', function () {
assert.throws(function () {
var promise = new RSVP.Promise(function () {}, {});
}, TypeError);
assert.throws(function () {
var promise = new RSVP.Promise(function () {}, 'boo!');
}, TypeError);
});
it('should reject on resolver exception', function(done) { it('should reject on resolver exception', function(done) {
var promise = RSVP.Promise(function() { var promise = RSVP.Promise(function() {
throw 'error'; throw 'error';
...@@ -1111,3 +1122,209 @@ if (typeof module !== 'undefined' && module.exports) { ...@@ -1111,3 +1122,209 @@ if (typeof module !== 'undefined' && module.exports) {
}); });
} }
describe("`CancellationError`", function () {
describe("`CancellationError` constructor", function () {
it('should exist and have length 1', function () {
assert(RSVP.CancellationError);
assert.equal(RSVP.CancellationError.length, 1);
});
it('should be a constructor', function () {
var error = new RSVP.CancellationError();
assert.equal(
Object.getPrototypeOf(error),
RSVP.CancellationError.prototype,
'[[Prototype]] equals CancellationError.prototype'
);
assert.equal(
error.constructor,
RSVP.CancellationError,
'constructor property of instances is set correctly'
);
assert.equal(
RSVP.CancellationError.prototype.constructor,
RSVP.CancellationError,
'constructor property of prototype is set correctly'
);
});
it('should throw a `TypeError` if not given a string', function () {
assert.throws(function () {
var error = new RSVP.CancellationError({});
// ... jslint forces to use the variable
error.foo = "bar";
}, TypeError);
});
it('should be throwable', function () {
assert.throws(function () {
throw new RSVP.CancellationError();
}, RSVP.CancellationError);
});
});
describe("`CancellationError` instance", function () {
it('must be an instance of Error', function () {
var error = new RSVP.CancellationError();
assert(error instanceof Error);
});
it('must have a `name` property with value `cancel`', function () {
var error = new RSVP.CancellationError();
assert.equal(error.name, "cancel");
});
it('must have a `message` property with the message value', function () {
var error = new RSVP.CancellationError("Foo");
assert.equal(error.message, "Foo");
});
it('must have a default `message` property to `Default Message`',
function () {
var error = new RSVP.CancellationError();
assert.equal(error.message, "Default Message");
});
});
});
describe("`cancel` on directly created promise", function () {
it('should be a function', function () {
var promise = new RSVP.Promise(function (done, fail) {});
assert.equal(typeof promise.cancel, "function");
});
it('should not call `onCancelled` with a `fulfilled` promise', function (done) {
var cancel_called = false,
promise = new RSVP.Promise(function (resolve) {
resolve();
}, function () {
cancel_called = true;
});
setTimeout(function() {
promise.cancel();
setTimeout(function() {
assert.equal(cancel_called, false);
done();
}, 20);
}, 20);
});
it('should not call `onCancelled` with a `rejected` promise', function (done) {
var cancel_called = false,
promise = new RSVP.Promise(function (resolve, reject) {
reject();
}, function () {
cancel_called = true;
});
setTimeout(function() {
promise.cancel();
setTimeout(function() {
assert.equal(cancel_called, false);
done();
}, 20);
}, 20);
});
describe("call on a `pending` promise", function () {
it('should call `onCancelled` without argument', function (done) {
var cancel_called = false,
promise = new RSVP.Promise(function () {
}, function () {
cancel_called = true;
assert.equal(arguments.length, 0);
});
setTimeout(function() {
promise.cancel("Foo");
setTimeout(function() {
assert.equal(cancel_called, true);
done();
}, 20);
}, 20);
});
it('should reject on canceller exception', function (done) {
var promise = RSVP.Promise(function() {}, function () {
throw 'bar error'
}),
error;
promise.then(null, function(e) {
error = e;
});
promise.cancel();
setTimeout(function() {
assert.equal(error, 'bar error');
done();
}, 20);
});
it('should reject with a CancellationError', function (done) {
var promise = RSVP.Promise(function() {}),
error;
promise.then(null, function(e) {
error = e;
});
promise.cancel();
setTimeout(function() {
assert(error instanceof RSVP.CancellationError);
done();
}, 20);
});
});
});
describe("`cancel` on promise created by then", function () {
it('should be a function', function () {
var promise = new RSVP.Promise(function (done, fail) {}).then();
assert.equal(typeof promise.cancel, "function");
});
it('should cancel the pending fulfilled promise', function (done) {
var cancel_called = false,
promise = new RSVP.Promise(function (resolve) {resolve();}),
promise2 = promise.then(function () {
return RSVP.Promise(function() {}, function () {
cancel_called = true;
});
});
setTimeout(function() {
promise2.cancel("Foo");
setTimeout(function() {
assert.equal(cancel_called, true);
assert.equal(promise2.isFulfilled, undefined);
assert.equal(promise2.isRejected, true);
assert(promise2.rejectedReason instanceof RSVP.CancellationError);
done();
}, 20);
}, 20);
});
it('should cancel the pending rejected promise', function (done) {
var cancel_called = false,
promise = new RSVP.Promise(function (resolve, reject) {reject();}),
promise2 = promise.then(undefined, function () {
return RSVP.Promise(function() {}, function () {
cancel_called = true;
});
});
setTimeout(function() {
promise2.cancel("Foo");
setTimeout(function() {
assert.equal(cancel_called, true);
assert.equal(promise2.isFulfilled, undefined);
assert.equal(promise2.isRejected, true);
assert(promise2.rejectedReason instanceof RSVP.CancellationError);
done();
}, 20);
}, 20);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment