Skip to content

Instantly share code, notes, and snippets.

@dmethvin
Last active August 29, 2015 14:02
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmethvin/43ffd1c743554e5c50ae to your computer and use it in GitHub Desktop.
Save dmethvin/43ffd1c743554e5c50ae to your computer and use it in GitHub Desktop.
$.xhr brainstorming
// Node-like signature with single callback, returns the XHR
$.xhrcb( url: String, complete: Function( err: Error, xhr: XHR, options: Object ) ): XHR;
$.xhrcb( options: Object, complete: Function( err: Error, xhr: XHR, options: Object ) ): XHR;
// Returns a Promise, throws an error if no Promise or shim
$.xhr( options: Object ): Promise
// See ticket http://bugs.jquery.com/ticket/14509 for `options`
// Thoughts:
// * `options` should not include callbacks! Expose functionality
// by passing in functions or chaining Promise.
// * May need to allow beforeSend option as an escape hatch.
// * `err` here does not try to judge success by HTTP status, that can
// be done at another level (see examples below).
// * I've separated out the entity data from the url data to avoid needing
// to guess where it goes and allow BOTH to be used in a request.
// ------------------- EXAMPLES -------------------
// Basic GET request, using complete callback
$.xhrcb({ url: "/user/1234" }, function( err, xhr, options ) {
if ( err || xhr.status !== 200 ) {
// Something went wrong, examine err and xhr
return;
}
var user = $.parseJSON( xhr.responseText );
// Note that `user` could be `null` here...
console.log( "Name: ", user.name );
});
// Use helper to break into $.ajax-like error/success callbacks
$.xhrcb("/user/1234", $.xhr.toSuccessError(
function( user, xhr, options ) {
console.log( "Name: ", user.name );
},
function( err, xhr, options ) {
console.log( "Error: " + err + " " + xhr.status );
}
));
// GET request known to return JSON, using Promise
$.xhr( "/user/123/" )
.then( $.xhr.toJSON )
.then( function( user ) {
console.log( "Name: ", user.name );
})
.catch( function( err ) {
console.error( "Error: ", err );
});
// POST request where we only care about success
$.xhr({
url: "/user/create/",
method: "POST",
body: { name: "Dave", awesome: true, city: "Columbia" }
})
.then(
function() {
console.log( "User created" );
},
function( err ) {
console.error( err.message );
}
);
// POST with a single retry if the server is busy. Assume we add the xhr
// and options as a property of the Error object so it can be accessed.
$.xhr({
url: "/user/create/",
method: "POST",
body: { name: "Dave", awesome: true, city: "Columbia" }
})
.catch( function( err ) {
// If the server was busy, retry once
if ( err.xhr && err.xhr.status === 408 ) {
return $.xhr( err.options );
}
// Some other error, re-throw and we'll catch below
throw( err );
})
.then( function() {
console.log( "User created" );
})
.catch( function( err ) {
// We could be specific with .options and .xhr if needed
console.error( err.message );
});
// GET where the caller wants us to guess type based on response,
// pretty much like $.ajax but with the type returned
$.xhr( "/user/123/" )
.then( $.xhr.toContentType )
.then( function( response ) {
if ( response.type !== "json" ) {
throw( "I WANT JSON" );
}
console.log( "Name: ", response.data.name );
})
.catch( function( err ) {
console.error( err );
});
// ------------------- Helpers -------------------
// Helper to create error/success callback based on HTTP code and
// convert types, similar to current jQuery $.ajax distinction.
// N.B. I think this should be a plugin rather than built in
jQuery.xhr.toSuccessError = function( successFn, errorFn ) {
return function( err, xhr, options ) {
// Call errorFn if err or xhr.status !== 2xx or conversion fails,
// otherwise call sucessFn with converted data
};
};
// Any $.xhr caller could also use xhr.response directly in XHR2
// as long as xhr.responseType was set before the call.
// (So should we do that as part of options processing?)
jQuery.xhr.toJSON = function( xhr ) {
return jQuery.parseJSON( xhr.responseText );
};
jQuery.xhr.toDOM = function( xhr ) {
return jQuery.parseHTML( xhr.responseText );
};
jQuery.xhr.toText = function( xhr ) {
return xhr.responseText;
};
jQuery.xhr.toScript = function( xhr ) {
return jQuery.globalEval( xhr.responseText );
};
jQuery.xhr.toContentType( xhr ) {
// Look at Content-Type of response to determine what to
// return, similar to "intelligent guess" in $.ajax
return {
type: "json",
data: { },
xhr: xhr
};
};
jQuery.xhr.factory = function() {
return new XMLHttpRequest();
};
@gnarf
Copy link

gnarf commented Jul 1, 2014

I really like where this is going, but the "sometimes promise" / "sometimes xhr" return on the method based on presence of "callback" kind of scares me.

@cowboy
Copy link

cowboy commented Jul 1, 2014

A few comments.

  • Is the Node-style callback signature a new thing that all of jQuery will have? It doesn't seem necessary in the context of The-jQuery-I-Know. I'm wondering what the motivation is.
  • The idea that a function can return two totally different things based on what gets passed in is very scary to me. I've done it before, and it's been very confusing for users.
  • This expects a global Promise object. Will jQuery include a shim?
  • I'd rename the jQuery.xhr.as* methods to jQuery.xhr.to*. I expect the usage of an asJSON method to be like $.xhr("/user/123/").asJSON() whereas a toJSON method feels better like $.xhr("/user/123/").then($.xhr.toJSON).

@scottgonzalez
Copy link

Initial thoughts:

I don't like different return types based on which parameters are passed.

I don't like $.xhr.toSuccessError(); just use the promise.

// GET request known to return JSON, using Promise
$.xhr( "/user/123/" )
    .then( $.xhr.asJSON )
    .then( function( user ) {
        console.log( "Name: ", user.name );
    })
    .catch( function( err ) {
        console.error( "Error: ", err );
    });

This should already be converted because the server should properly indicate the data type in the header.

    .catch( function( err ) {
        // If the server was busy, retry once
        if ( err.xhr && err.xhr.status === 408 ) {
            return $.xhr( err.options );
        }
        // Some other error, re-throw and we'll catch below
        throw( err );
    })

I really like that a retry should be possible by reusing the options. I do something similar with my GitHub request API: https://github.com/scottgonzalez/hookup/blob/master/lib/hookup.js#L84-L101

 // GET where the caller wants us to guess type based on response,
 // pretty much like $.ajax but with the type returned
 $.xhr( "/user/123/" )
    .then( $.xhr.asContentType )

Why is this not the default behavior?

    // N.B.: Try to avoid the need to make a copy of incoming options
    options = typeof options === "string" ? { url: options } : options;

I'm not sure that we should avoid that. Since options continue to get passed around after the request, this can easily introduce bugs in user code that are extremely hard to track down.

We should review uses of beforeSend to make sure we're not losing any functionality. This was on the list of options to keep.

@dmethvin
Copy link
Author

dmethvin commented Jul 9, 2014

Sorry about missing these comments, I had forgotten that Github doesn't have notifications. Let's see if mentions generate a notification.

@cowboy, @gnarf, @scott_gonzalez, I agree and in discussions it seems we're settling on having the API return one thing rather than depending on its inputs.

I don't like $.xhr.toSuccessError(); just use the promise.

That would mean we need to apply the value judgements to the replies in order to get a success/error, which brings us back to internalizing a bunch of default decisions that would need to be overridden at times. For example, a HTTP 200 response is not success if the Content-type is JSON and the JSON is invalid, and a non-2xx request won't have its content (script, JSON, XML) processed even if there's a valid Content-type since we always throw that to the error case and there's no arg for the data in the error callback anyway.

There may be a better way to name or chain these as promises, but the idea here is that these become explicit dependencies rather than hiding inside the options. That way they can be tracked and included/excluded as needed.

This should already be converted because the server should properly indicate the data type in the header.

Currently we allow it to be overridden by dataType, and need that because we convert it internally via "intelligent guess". I'm advocating that we make that explicit, the user either chains a promise and calls a helper to convert it to the type they know it is, or calls an "intelligent guess" wheel of fortune helper.

We should review uses of beforeSend to make sure we're not losing any functionality. This was on the list of options to keep

Yeah, that's the one callback we may need, but on the bright side it might also allow us to avoid several other request-munging options that aren't too common.

@dmethvin
Copy link
Author

Gist updated ...

@tarikbenmerar
Copy link

Hello I have some suggestions and equiries here :

1- I have seen that some javascript libraries can transform any thenable to a promise. Take for example : https://github.com/cujojs/when/blob/master/docs/api.md#when. I was thinking of a small thenable (Acting like an equivalent callback) version that can be coerced to a promise. There is some mechanism in every library, including W3C version of Promise : https://github.com/domenic/promises-unwrapping. At "compilation" step, any promise library can be plugged in, there is any dependance on any particular library.

2- Is there a way to have a factory mechanism to xhr ? So that, you can create a sandboxed version of the object and have a scoped set of default configurations. $.xhrFactory(defaultSettings).

3- How can we access the base XHR ? Thinking of it, it made me thinking why don't we have just a lightweight mechanism like ?

  • $.xhr( XHR ).set({url:, type: }).request().then(successCallBack, errorCallBack);
    or like this ?
  • var jqXHR = $.xhr({url: type});
    jqXHR.get('url');
    jqXHR.set({url: 'request/'});
    jqXHR.request().then(function () { });

Simply, having xhr as a thin layer to the base XHR, that simplify its programming in a cross browser way. That's my personal opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment