Rejecting / Resolving a promise from outside its body - ember.js

I have a need to reject a promise from outside its body, to handle the case of the user that wanted to cancel the action.
Here, I need to start several uploads at the same time, by calling #start on every queued uploads.
The class that manages the uploads queue then stores all the promises and uses Ember.RSVP.all to handle when all the promises have resolved or one has rejected. This works fine.
Now, I would like to cancel the upload
App.Upload = Ember.Object.extend({
start: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
self.startUpload() // Async upload with jQuery
.then(
function(resp) { resolve(resp) },
function(error) { reject(error) }
);
});
},
cancel: function() {
this.get('currentUpload').cancel() // Works, and cancels the upload
// Would like to reject the promise here
},
startUpload: function() {
return this.set('currentUpload', /* some jqXHR that i build to upload */)
}
});
I have thought of many ways to handle it, but I don't found any method like myPromise.reject(reason).
So what I did, is to store the reject function in the Upload instance and call it from my cancel method, like this :
App.Upload = Ember.Object.extend({
start: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
/* Store it here */
self.set('rejectUpload', reject);
/* ------------- */
self.startUpload() // Async upload with jQuery
.then(
function(resp) { resolve(resp) },
function(error) { reject(error) }
);
});
},
cancel: function() {
this.get('currentUpload').cancel() // Works, and cancels the upload
/* Reject the promise here */
var reject;
if (reject = this.get('rejectUpload')) reject();
/* ----------------------- */
},
startUpload: function() {
return this.set('currentUpload', /* some jqXHR that i build to upload */)
}
});
This sound a bit dirty to me, and I'd like to know if there was a better way to make this.
Thanks for your time !

var deferred = Ember.RSVP.defer();
deferred.resolve("Success!");
deferred.reject("End of the world");
To access the promise (for thening etc)
deferred.promise.then(function(){
console.log('all good');
},function(){
console.log('all bad');
});

Related

Using fetch inside an action within my component

I'm curious about how I could implement this, I'd like to not hit this API every time the page loads on the route, but would rather start the call on an action (I suppose this action could go anywhere, but it's currently in a component). I'm getting a server response, but having trouble getting this data inside my component/template. Any ideas? Ignore my self.set property if I'm on the wrong track there....Code below..Thanks!
import Component from '#ember/component';
export default Component.extend({
res: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer JO5Wxxxxxxxxx'
}
}).then(function(response) {
self.set('res', response.json());
return response.json();
});
}
}
});
Solved below...
import Component from '#ember/component';
export default Component.extend({
flightResults: null,
actions: {
searchFlight(term) {
let self = this;
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
return fetch(url, {
headers: {
'Content-Type': 'application/vnd.amadeus+json',
'Authorization':'Bearer xxxxxxxxxxxxxx'
}
}).then(function(response) {
return response.json();
}).then(flightResults => {
this.set('flightResults', flightResults);
});
}
}
});
You might find ember-concurrency to be useful in this situation. See the example of "Type-ahead search", modified for your example:
const DEBOUNCE_MS = 250;
export default Controller.extend({
flightResults: null;
actions: {
searchFlight(term) {
this.set('flightResults', this.searchRepo(term));
}
},
searchRepo: task(function * (term) {
if (isBlank(term)) { return []; }
// Pause here for DEBOUNCE_MS milliseconds. Because this
// task is `restartable`, if the user starts typing again,
// the current search will be canceled at this point and
// start over from the beginning. This is the
// ember-concurrency way of debouncing a task.
yield timeout(DEBOUNCE_MS);
let url = `https://test.api.amadeus.com/v1/shopping/flight-offers?origin=PAR&destination=LON&departureDate=2018-09-25&returnDate=2018-09-28&adults=1&travelClass=BUSINESS&nonStop=true&max=2`;
// We yield an AJAX request and wait for it to complete. If the task
// is restarted before this request completes, the XHR request
// is aborted (open the inspector and see for yourself :)
let json = yield this.get('getJSON').perform(url);
return json;
}).restartable(),
getJSON: task(function * (url) {
let xhr;
try {
xhr = $.getJSON(url);
let result = yield xhr.promise();
return result;
// NOTE: could also write this as
// return yield xhr;
//
// either way, the important thing is to yield before returning
// so that the `finally` block doesn't run until after the
// promise resolves (or the task is canceled).
} finally {
xhr.abort();
}
}),
});

Ember-cli throwing Unexpected Token error

Undoubtedly this error is something easy for an ember expert to identify but thats not me so here it is
Ember-cli identifies blank space before this line as an unexpected token:
this.store = container.lookup('store:main');
/*global md5*/
import Ember from 'ember';
// Since I've defined my url in environment.js I can do this
import ENV from '../config/environment';
var ref = new window.Firebase(ENV.firebaseURL);
export default {
name: 'session',
// Run the initializer after the store is ready
after: 'store',
initialize: function(container, app) {
// session object is nested here as we need access to the container to get the store
var session = Ember.Object.extend({
// initial state
authed: false,
// get access to the ember data store
//Here is the offending line
this.store = container.lookup('store:main');
init: function() {
// on init try to login
ref.onAuth(function(authData) {
// Not authenticated
if (!authData) {
this.set('authed', false);
this.set('authData', null);
this.set('user', null);
return false;
}
// Authenticated
this.set('authed', true);
this.set('authData', authData);
this.afterAuthentication(authData.uid);
}.bind(this));
},
// Call this from your Ember templates
login: function(provider) {
this._loginWithPopup(provider);
},
// Call this from your Ember templates
logout: function() {
ref.unauth();
},
// Default login method
_loginWithPopup: function(provider) {
var _this = this;
// Ember.debug('logging in with popup');
ref.authWithOAuthPopup(provider, function(error, authData) {
if (error) {
if (error.code === "TRANSPORT_UNAVAILABLE") {
// fall-back to browser redirects, and pick up the session
// automatically when we come back to the origin page
_this._loginWithRedirect(provider);
}
} else if (authData) {
// we're good!
// this will automatically call the on ref.onAuth method inside init()
}
});
},
// Alternative login with redirect (needed for Chrome on iOS)
_loginWithRedirect: function(provider) {
ref.authWithOAuthRedirect(provider, function(error, authData) {
if (error) {
} else if (authData) {
// we're good!
// this will automatically call the on ref.onAuth method inside init()
}
});
},
// Runs after authentication
// It either sets a new or already exisiting user
afterAuthentication: function(userId) {
var _this = this;
// See if the user exists using native Firebase because of EmberFire problem with "id already in use"
ref.child('users').child(userId).once('value', function(snapshot) {
var exists = (snapshot.val() !== null);
userExistsCallback(userId, exists);
});
// Do the right thing depending on whether the user exists
function userExistsCallback(userId, exists) {
if (exists) {
_this.existingUser(userId);
} else {
_this.createUser(userId);
}
}
},
// Existing user
existingUser: function(userId) {
this.store.find('user', userId).then(function(user) {
_this.set('user', user);
}.bind(this));
},
// Create a new user
createUser: function(userId) {
var _this = this;
this.get('store').createRecord('user', {
id: userId,
provider: this.get('authData.provider'),
name: this.get('authData.facebook.displayName') || this.get('authData.google.displayName'),
email: this.get('authData.facebook.email') || this.get('authData.google.email'),
created: new Date().getTime()
}).save().then(function(user){
// Proceed with the newly create user
_this.set('user', user);
});
},
// This is the last step in a successful authentication
// Set the user (either new or existing)
afterUser: function(user) {
this.set('user', user);
}
});
// Register and inject the 'session' initializer into all controllers and routes
app.register('session:main', session);
app.inject('route', 'session', 'session:main');
app.inject('controller', 'session', 'session:main');
}
};
You're calling Ember.Object.extend with an Javascript Object literal what you are trying to do is invalid javascript syntax.
You'll probably want to stick that line in your init function.
init: function() {
//Here is the offending line
this.store = container.lookup('store:main');
...
When you get an invalid token error message you're writing something the javascript compiler doesn't understand.

How to continue even if Ember.js model hook doesn't load all promises?

I'm loading a route. Its model hook loads some models. Some are fetch from ember store and some are promises requested through AJAX:
model: function () {
return Em.RSVP.hash({
//the server data might not be loaded if user is offline (application runs using appcache, but it's nice to have)
someServerData: App.DataService.get(),
users: this.store.find('user')
});
}
The App.DataService.get() is defined as:
get: function () {
return new Ember.RSVP.Promise(function(resolve, reject) {
//ajax request here
});
}
Obviously if the request is rejected, the flow is interrupted and I cannot display the page at all.
Is there a way to overcome this?
Ember.RSVP.hashSettled is exactly meant for this purpose.
From tildeio/rsvp.js Github repository:
hashSettled() work exactly like hash(), except that it fulfill with a hash of the constituent promises' result states. Each state object will either indicate fulfillment or rejection, and provide the corresponding value or reason. The states will take one of the following formats:
{ state: 'fulfilled', value: value }
or
{ state: 'rejected', reason: reason }
Here is an example for using it (working JS Bin example):
App.IndexRoute = Ember.Route.extend({
fallbackValues: {
firstProperty: null,
secondProperty: null
},
model: function() {
var fallbackValues = this.get('fallbackValues');
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.RSVP.hashSettled({
firstProperty: Ember.RSVP.Promise.resolve('Resolved data despite error'),
secondProperty: (function() {
var doomedToBeRejected = $.Deferred();
doomedToBeRejected.reject({
error: 'some error message'
});
return doomedToBeRejected.promise();
})()
}).then(function(result) {
var objectToResolve = {};
Ember.keys(result).forEach(function(key) {
objectToResolve[key] = result[key].state === 'fulfilled' ? result[key].value : fallbackValues[key];
});
resolve(objectToResolve);
}).catch(function(error) {
reject(error);
});
});
}
});
fallbackValues can be useful for managing resolved hash's properties' fallback values without using conditions inside the promise function.
Taking into account that Ember.RSVP.hashSettled is not available in my Ember version. I come up with the following solution:
model: function(params) {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject){
// get data from server
App.DataService.get().then(function(serverData) { //if server responds set it to the promise
resolve({
serverData: serverData,
users: self.store.find('user')
});
}, function(reason){ //if not ignore it, and send the rest of the data
resolve({
users: self.store.find('user')
});
});
});
}

ember-cli custom authenticator simple auth session authentication failed

What do I need to add to the code to initiate the sessionAuthenticationFailed(error). Right now it works when I have a successful login but I would like it also to show a message when when an incorrect username and/or password is entered.
here is what I have within authenticate in my custom authenticator
authenticate: function(credentials) {
var _this = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.post( _this.serverTokenEndpoint, {
email: credentials.identification,
password: credentials.password
}).then(function(response) {
Ember.run(function() {
resolve({ token: response.session.token });
});
}, function(xhr, status, error) {
var response = JSON.parse(xhr.responseText);
Ember.run(function() {
reject(response.error);
});
});
});
}
I would also like to show an error message. What do I need to put in my loginController.
The session's authenticate method returns a promise. You can attach a then to that and handle it accordingly in your controller, e.g.:
this.get('session').authenticate('authenticator', { … }).then(function() { /*success*/ }, function() { /* error */ });
or if you're using the LoginControllerMixin:
export Ember.Route.extend(LoginControllerMixin, {
actions: {
authenticate: function() {
this._super().then(function() { /*success*/ }, function() { /* error */ });
}
}
});
The sessionAuthenticationFailed should be called automatically anyway whenever authentication fails but if you want to e.g. display an error message when authentication fails etc. I'd use above approach.

How to execute Ember.RSVP.all within an ember run loop correctly

I'm trying to execute a promise inside Ember.RSVP.all
App.Foo = Ember.Object.create({
bar: function() {
var configuration = ajaxPromise("/api/configuration/", "GET");
Ember.RSVP.all([configuration]).then(function(response) {
//do something with the response in here
});
}
});
But because my integration test mocks the xhr w/out a run loop the test fails with the expected error "You have turned on testing mode, which disabled the run-loop' autorun"
So I wrapped the RSVP with a simple ember.run like so
App.Foo = Ember.Object.create({
bar: function() {
var configuration = ajaxPromise("/api/configuration/", "GET");
Ember.run(function() {
Ember.RSVP.all([configuration]).then(function(response) {
//do something with the response in here
});
});
}
});
But I still get the error for some odd reason. Note -if I run later it's fine (this won't work though as I need to exec the async code for this test to work correctly)
App.Foo = Ember.Object.create({
bar: function() {
var configuration = ajaxPromise("/api/configuration/", "GET");
Ember.run.later(function() {
Ember.RSVP.all([configuration]).then(function(response) {
//do something with the response in here
});
});
}
});
Here is my ajaxPromise implementation -fyi
var ajaxPromise = function(url, type, hash) {
return new Ember.RSVP.Promise(function(resolve, reject) {
hash = hash || {};
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.success = function(json) {
Ember.run(null, resolve, json);
};
hash.error = function(json) {
Ember.run(null, reject, json);
};
$.ajax(hash);
});
}
How can I wrap the Ember.RVSP inside my ember run w/out it throwing this error?
Update
here is my test setup (including my helper)
document.write('<div id="ember-testing-container"><div id="wrap"></div></div>');
App.setupForTesting();
App.injectTestHelpers();
test("test this async stuff works", function() {
visit("/").then(function() {
equal(1, 1, "omg");
});
});
The only part I've left out is that I'm using jquery-mockjax so no run loop wraps the xhr mock (and in part that's why I like this library, it fails a test when I don't wrap async code with a run loop as the core team suggests)
This may have to do with how your tests are being run, so if you can provide the test, it will be helpful
I also noticed:
It turns out I believe you are also being (or will be soon) trolled by jQuery's jQXHR object being a malformed promise, the fulfills with itself for 0 reason, and enforcing its own nextTurn on you. Which is causing the autorun. This will only happen in the error scenario.
In ember data we sort this out, by stripping the then off the jQXHR object
see:
https://github.com/emberjs/data/blob/4bca3d7e86043c7c5c4a854052a99dc2b4089be7/packages/ember-data/lib/adapters/rest_adapter.js#L539-L541
I suspect the following will clear this up.
var ajaxPromise = function(url, type, hash) {
return new Ember.RSVP.Promise(function(resolve, reject) {
hash = hash || {};
hash.url = url;
hash.type = type;
hash.dataType = 'json';
hash.success = function(json) {
Ember.run(null, resolve, json);
};
hash.error = function(json) {
if (json && json.then) { json.then = null } // this line
Ember.run(null, reject, json);
};
$.ajax(hash);
});
}
This is rather unfortunate, and various separate concepts and ideas are coming together to cause you pain. We hope to (very shortly) land Ember.ajax which normalizes all these crazy away.
Also feel free to checkout how ember-data is going this: https://github.com/emberjs/data/blob/4bca3d7e86043c7c5c4a854052a99dc2b4089be7/packages/ember-data/lib/adapters/rest_adapter.js#L570-L586
I feel your pain on this Toran, I'm sure it's what Stefan's stated, we had to 1 off mockjax to get our tests to work with it.
https://github.com/kingpin2k/jquery-mockjax/commit/ccd8df8ed7f64672f35490752b95e527c09931b5
// jQuery < 1.4 doesn't have onreadystate change for xhr
if ($.isFunction(onReady)) {
if (mockHandler.isTimeout) {
this.status = -1;
}
Em.run(function () {
onReady.call(self, mockHandler.isTimeout ? 'timeout' : undefined);
});
} else if (mockHandler.isTimeout) {
// Fix for 1.3.2 timeout to keep success from firing.
this.status = -1;
}